From 6e5542e142777a689b019cd2ed4a1202c1b09601 Mon Sep 17 00:00:00 2001 From: Sean Stewart Date: Sun, 7 Jul 2024 10:22:56 -0400 Subject: [PATCH] docs: update README.md and docstrings --- README.md | 6 ++--- src/typelib/codec.py | 39 +++++++++++++++++++++++++++ src/typelib/future.py | 2 +- src/typelib/interchange.py | 54 +++++++++++++++++++++++++++++++++++--- 4 files changed, 94 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fa4ddd1..c4f3b54 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ import uuid from typing import TypedDict -from typelib import format, compat +from typelib import interchange, compat from app import models @@ -115,8 +115,8 @@ 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 = {} diff --git a/src/typelib/codec.py b/src/typelib/codec.py index 377cfe6..bd6699f 100644 --- a/src/typelib/codec.py +++ b/src/typelib/codec.py @@ -1,3 +1,5 @@ +"""Interfaces for managing type-enforced wire protocols (codecs).""" + from __future__ import annotations import abc @@ -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__( @@ -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.""" diff --git a/src/typelib/future.py b/src/typelib/future.py index bd5b4ef..92d481c 100644 --- a/src/typelib/future.py +++ b/src/typelib/future.py @@ -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. diff --git a/src/typelib/interchange.py b/src/typelib/interchange.py index def149f..6cd10f8 100644 --- a/src/typelib/interchange.py +++ b/src/typelib/interchange.py @@ -1,3 +1,5 @@ +"""Interface for marshalling, unmarshalling, encoding, and decoding data to and from a bound type.""" + from __future__ import annotations import dataclasses @@ -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: @@ -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 @@ -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."""