Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap backend-specific exceptions in SerializationError/DeserializationError #57

Merged
merged 3 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading