Skip to content

Commit

Permalink
Wrap backend-specific exceptions in SerializationError/Deserializatio…
Browse files Browse the repository at this point in the history
…nError (#57)
  • Loading branch information
agronholm authored Dec 22, 2024
1 parent e234c96 commit 87784f4
Show file tree
Hide file tree
Showing 15 changed files with 143 additions and 97 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
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx_autodoc_typehints",
"sphinx_rtd_theme",
]

templates_path = ["_templates"]
Expand Down
40 changes: 7 additions & 33 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,19 @@ any necessary configuration values for it. The following backends are provided o

Other backends may be provided by other components.

Once you've selected a backend, see its specific documentation to find out what configuration
values you need to provide, if any. Configuration values are expressed as constructor arguments
for the backend class::
Once you've selected a backend, see its specific documentation to find out what
configuration values you need to provide, if any. Configuration values are expressed as
constructor arguments for the backend class::

components:
serialization:
backend: json

This configuration publishes a :class:`~.api.Serializer` resource named
``default`` using the JSON backend. The same can be done directly in Python code as
follows:
This configuration publishes a :class:`~Serializer` resource named ``default`` using the
JSON backend. The same can be done directly in Python code as follows:

.. code-block:: python
class ApplicationComponent(ContainerComponent):
async def start(ctx: Context) -> None:
class ApplicationComponent(Component):
def __init__(self) -> None:
self.add_component('serialization', backend='json')
await super().start()
Multiple serializers
--------------------

If you need to configure multiple serializers, you will need to use multiple instances
of the serialization component::

components:
serialization:
backend: cbor
serialization/msgpack:
resource_name: msgpack
backend: msgpack

The above configuration creates two serializer resources, available under 6 different
combinations:

* :class:`~.api.Serializer` / ``default``
* :class:`~.api.CustomizableSerializer` / ``default``
* :class:`~.serializers.cbor.CBORSerializer` / ``default``
* :class:`~.api.Serializer` / ``msgpack``
* :class:`~.api.CustomizableSerializer` / ``msgpack``
* :class:`~.serializers.msgpack.MsgpackSerializer` / ``msgpack``
46 changes: 21 additions & 25 deletions docs/extending.rst
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
Writing new serializer backends
===============================

If you wish to implement an alternate method of serialization, you can do so by subclassing
the :class:`~asphalt.serialization.api.Serializer` class.
There are three methods implementors must override:
.. py:currentmodule:: asphalt.serialization
* :meth:`~asphalt.serialization.api.Serializer.serialize`
* :meth:`~asphalt.serialization.api.Serializer.deserialize`
* :meth:`~asphalt.serialization.api.Serializer.mimetype`
If you wish to implement an alternate method of serialization, you can do so by
subclassing the :class:`~Serializer` class. There are three methods implementors must
override:

The ``mimetype`` method is a ``@property`` that simply returns the MIME type appropriate for the
serialization scheme. This property is used by certain other components. If you cannot find an
applicable MIME type, you can use ``application/octet-stream``.
* :meth:`~Serializer.serialize`
* :meth:`~Serializer.deserialize`
* :meth:`~Serializer.mimetype`

The ``mimetype`` method is a ``@property`` that simply returns the MIME type appropriate
for the serialization scheme. This property is used by certain other components. If you
cannot find an applicable MIME type, you can use ``application/octet-stream``.

.. note:: Serializers must always serialize to bytes; never serialize to strings!

If you want your serializer to be available as a backend for
:class:`~asphalt.serialization.component.SerializationComponent`, you need to add the corresponding
entry point for it. Suppose your serializer class is named ``AwesomeSerializer``, lives in the
package ``foo.bar.awesome`` and you want to give it the alias ``awesome``, add this line to your
project's ``setup.py`` under the ``entry_points`` argument in the
``asphalt.serialization.serializers`` namespace:

.. code-block:: python
setup(
# (...other arguments...)
entry_points={
'asphalt.serialization.serializers': [
'awesome = foo.bar.awesome:AwesomeSerializer'
]
}
)
:class:`~asphalt.serialization.SerializationComponent`, you need to add the
corresponding entry point for it. Suppose your serializer class is named
``AwesomeSerializer``, lives in the package ``foo.bar.awesome`` and you want to give it
the alias ``awesome``, add this line to your project's ``pyproject.toml`` under the
``entry_points`` argument in the ``asphalt.serialization.serializers`` namespace:

.. code-block:: toml
[project.entry-points."asphalt.serialization.serializers"]
awesome = "foo.bar.awesome:AwesomeSerializer"
8 changes: 6 additions & 2 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
Version history
===============

This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
This library adheres to `Semantic Versioning 2.0 <https://semver.org/>`_.

**UNRELEASED**

- Dropped support for Python 3.7
- 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
26 changes: 13 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ addopts = "-rsx --tb=short"
testpaths = ["tests"]

[tool.mypy]
python_version = "3.8"
python_version = "3.9"
strict = true
explicit_package_bases = true

Expand All @@ -96,18 +96,18 @@ branch = true
show_missing = true

[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py38, py39, py310, py311, py312, py313, pypy3
env_list = ["py39", "py310", "py311", "py312", "py313", "pypy3"]
skip_missing_interpreters = true
minversion = 4.4.3

[testenv]
extras = test
commands = python -m pytest {posargs}
package = editable
[tool.tox.env_run_base]
commands = [["python", "-m", "pytest", { replace = "posargs", extend = true }]]
package = "editable"
extras = ["test"]

[testenv:docs]
extras = doc
commands = sphinx-build -n docs build/sphinx
"""
[tool.tox.env.pyright]
deps = ["pyright"]
commands = [["pyright", "--verifytypes", "asphalt.exceptions"]]

[tool.tox.env.docs]
commands = [["sphinx-build", "-W", "-n", "docs", "build/sphinx", { replace = "posargs", extend = true }]]
extras = ["doc"]
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 DeserializationError: 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
Loading

0 comments on commit 87784f4

Please sign in to comment.