Skip to content

Commit

Permalink
docs: update README.md and docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
seandstewart committed Jul 7, 2024
1 parent eab327c commit 6e5542e
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 7 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,16 @@ import uuid

from typing import TypedDict

from typelib import format, compat
from typelib import interchange, compat

from app import models


class ClientRPC:

def __init__(self):
self.business_repr = format.protocol(models.BusinessModel)
self.client_repr = format.protocol(ClientRepresentation)
self.business_repr = interchange.protocol(models.BusinessModel)
self.client_repr = interchange.protocol(ClientRepresentation)
self.db = {}


Expand Down
39 changes: 39 additions & 0 deletions src/typelib/codec.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Interfaces for managing type-enforced wire protocols (codecs)."""

from __future__ import annotations

import abc
Expand All @@ -11,12 +13,31 @@


class AbstractCodec(abc.ABC, t.Generic[T]):
"""The abstract interface for defining a wire protocol (codec).
Developers may define custom codecs with this interface which are compatible with
:py:func:`typelib.interchange.protocol`.
See Also:
* :py:func:`typelib.interchange.protocol`
"""

def encode(self, value: T) -> bytes: ...

def decode(self, value: bytes) -> T: ...


class Codec(AbstractCodec[T], t.Generic[T]):
"""A standard wire protocol (codec).
This codec wraps the encoding and decoding of data to/from bytes with marshalling
and unmarshaling capabilities, allowing you to serialize and deserialize data directly
to/from your in-memory data models.
See Also:
* :py:func:`typelib.interchange.protocol`
"""

__slots__ = ("marshaller", "unmarshaller", "encoder", "decoder")

def __init__(
Expand All @@ -33,15 +54,33 @@ def __init__(
self.decoder = decoder

def encode(self, value: T) -> bytes:
"""Encode an instance of `T` to bytes.
We will first marshal the given instance using :py:attr:`marshaller`, then
encode the marshalled data into bytes using :py:attr:`encoder`.
Args:
value: The instance to encode.
"""
marshalled = self.marshaller(value)
encoded = self.encoder(marshalled)
return encoded

def decode(self, value: bytes) -> T:
"""Decode an instance of `T` from bytes.
We will first decode the data from bytes using :py:attr:`decoder`, then
unmarshal the data into an instance of `T` using :py:attr:`unmarshaller`.
Args:
value: The bytes to decode.
"""
decoded = self.decoder(value)
unmarshalled = self.unmarshaller(decoded)
return unmarshalled


EncoderT: t.TypeAlias = t.Callable[[serdes.MarshalledValueT], bytes]
"""Protocol for a wire serializer."""
DecoderT: t.TypeAlias = t.Callable[[bytes], serdes.MarshalledValueT]
"""Protocol for a wire deserializer."""
2 changes: 1 addition & 1 deletion src/typelib/future.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

@functools.cache
def transform(annotation: str, *, union: str = "typing.Union") -> str:
"""Transform a :py:class:`UnionType` (``str | int``) into a :py:class:`typing.Union`.
"""Transform a :py:class:`types.UnionType` (``str | int``) into a :py:class:`typing.Union`.
Args:
annotation: The annotation to transform, as a string.
Expand Down
54 changes: 51 additions & 3 deletions src/typelib/interchange.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Interface for marshalling, unmarshalling, encoding, and decoding data to and from a bound type."""

from __future__ import annotations

import dataclasses
Expand All @@ -15,10 +17,50 @@
def protocol(
t: type[T],
*,
codec: mcodec.AbstractCodec[T] | None = None,
marshaller: mmarshal.AbstractMarshaller[T] | None = None,
unmarshaller: munmarshal.AbstractUnmarshaller[T] | None = None,
codec: mcodec.AbstractCodec[T] | None = None,
encoder: mcodec.EncoderT = compat.json.dumps,
decoder: mcodec.DecoderT = compat.json.loads,
) -> InterchangeProtocol[T]:
"""Factory function for creating an :py:class:`InterchangeProtocol` instance.
Notes:
In the simplest case, all that needs be provided is :py:param:`t`. We will
generate a marshaller, unmarshaller and codec. In most cases, you probably
don't need to override the default marshaller and unmarshaller behavior.
If no :py:param:`codec` is passed, we create a :py:class:`typelib.codec.Codec`
instance with :py:param:`marshaller`, :py:param:`unmarshaller`, :py:param:`encoder`
and :py:param:`decoder`. This codec instance combines your marshalling protocol
and your wire protocol, allowing you to pass instances of :py:param:`t` directly
to :py:meth:`~typelib.codec.Codec.encode` and recieve instances of :py:param:`t`
directly from :py:meth:`~typelib.codec.Codec.decode`.
The :py:param:`encoder` and :py:param:`decoder` default to JSON, using either
stdlib :py:mod:`json` or :py:mod:`orjson` if available.
You can customize your wire protocol in two ways:
1. Pass in a custom :py:class:`typelib.codec.AbstractCodec` instance.
* This will override the behavior described above. Useful when you have
your own optimized path for your wire protocol and don't desire our
marshalling capabilities.
2. Pass in custom :py:param:`encoder` and :py:param:`decoder` values.
* This will follow the behavior described above. Useful when you use a
wire protocol other than JSON.
Args:
t: The type to create the interchange protocol for.
codec: The codec for encoding and decoding data for over-the-wire (optional).
marshaller: The marshaller used to marshal inputs into the associated type. (optional)
unmarshaller: The unmarshaller used to unmarshal inputs into the associated type. (optional)
encoder: The encoder for encoding data for over-the-wire (defaults to JSON).
decoder: The decoder for decoding data from over-the-wire (defaults to JSON).
See Also:
* :py:mod:`typelib.codec`
"""
marshal = marshaller or mmarshal.marshaller(typ=t)
unmarshal = unmarshaller or munmarshal.unmarshaller(typ=t)
if inspection.isbytestype(t) and codec is None:
Expand All @@ -31,8 +73,8 @@ def protocol(
codec = codec or mcodec.Codec(
marshaller=marshal,
unmarshaller=unmarshal,
encoder=compat.json.dumps,
decoder=compat.json.loads,
encoder=encoder,
decoder=decoder,
)
proto = InterchangeProtocol(t=t, marshal=marshal, unmarshal=unmarshal, codec=codec)
return proto
Expand All @@ -44,7 +86,13 @@ def protocol(
@classes.slotted(dict=False, weakref=True)
@dataclasses.dataclass
class InterchangeProtocol(tp.Generic[T]):
"""The protocol for marshalling, unmarshalling, encoding and decoding data (interchange)."""

t: type[T]
"""The bound type definition for this protocol."""
marshal: mmarshal.AbstractMarshaller[T]
"""Callable which will marshal `T` instances into primitive types for serialization."""
unmarshal: munmarshal.AbstractUnmarshaller[T]
"""Callable which will unmarshal primitive types into `T` instances."""
codec: mcodec.AbstractCodec[T]
"""The wire protocol for encoding and decoding binary data."""

0 comments on commit 6e5542e

Please sign in to comment.