Skip to content

Commit

Permalink
Full support of typing.Self as a serializable type-hint.
Browse files Browse the repository at this point in the history
- Required rewriting some of the container serializers (Array, Union) to properly
  handle the fact that recursion (and thus re-use mid-pack/unpack) of a serializer
  can happen now.
  • Loading branch information
lojack5 committed Mar 21, 2024
1 parent ea1901a commit e5d3f9f
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 59 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ All other types fall into the "complex" category. They currently consist of:
- `unions`: Unions of serializable types are supported as well.
- `Structured`-derived types: You can use any of your `Structured`-derived classes as a type-hint,
and the variable will be serialized as well.
- `typing.Self`: This type-hint denotes that the attribute should be unpacked as an instance of
the containing class itself. Note that due to the recursive posibilities this allows, care
must be taken to avoid hitting the recursion limit of Python.


### Tuples
Expand Down
1 change: 1 addition & 0 deletions structured/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
from .structured import *
from .tuples import *
from .unions import *
from .self import *
11 changes: 11 additions & 0 deletions structured/serializers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
- Modified packing method `pack`
- All unpacking methods may return an iterable of values instead of a tuple.
For more details, check the docstrings on each method or attribute.
A note on "container" serializers (for example, CompoundSerializer and
ArraySerializer): Due to the posibility of recursive nesting via the
`typing.Self` type-hint as a serializable type, care must be taken with
delegating to sub-serializers. In particular, only updating `self.size` at the
*end* of a pack/unpack operation ensures that nested usages of the same
serializer won't overwrite intermediate values.
Similarly (although this is true regardless of nesting), you almost always want
a custom `prepack` and `preunpack` method, to pass that information along to
the nested serializers.
"""

from __future__ import annotations
Expand Down
58 changes: 38 additions & 20 deletions structured/serializers/arrays.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,26 +96,38 @@ def _check_data_size(self, expected: int, actual: int) -> None:
raise ValueError(
f'Array data size {actual} does not match expected size {expected}'
)

def prepack(self, partial_object) -> Self:
self._partial_object = partial_object
return self

def preunpack(self, partial_object) -> Self:
self._partial_object = partial_object
return self

def pack(self, *values: Unpack[tuple[list[T]]]) -> bytes:
data = [b'']
self.size = header_size = self.header_serializer.size
size = header_size = self.header_serializer.size
item_serializer = self.item_serializer.prepack(self._partial_object)
for item in values[0]:
data.append(self.item_serializer.pack(item))
self.size += self.item_serializer.size
header_values = self._header_pack_values(values[0], self.size - header_size)
data.append(item_serializer.pack(item))
size += item_serializer.size
header_values = self._header_pack_values(values[0], size - header_size)
data[0] = self.header_serializer.pack(*header_values)
self.size = size
return b''.join(data)

def pack_into(
self, buffer: WritableBuffer, offset: int, *values: Unpack[tuple[list[T]]]
) -> None:
items = values[0]
self.size = header_size = self.header_serializer.size
size = header_size = self.header_serializer.size
item_serializer = self.item_serializer.prepack(self._partial_object)
for item in items:
self.item_serializer.pack_into(buffer, offset + self.size, item)
self.size += self.item_serializer.size
header_values = self._header_pack_values(items, self.size - header_size)
item_serializer.pack_into(buffer, offset + size, item)
size += item_serializer.size
header_values = self._header_pack_values(items, size - header_size)
self.size = size
self.header_serializer.pack_into(buffer, offset, *header_values)

def pack_write(self, writable: BinaryIO, *values: Unpack[tuple[list[T]]]) -> None:
Expand All @@ -125,34 +137,40 @@ def pack_write(self, writable: BinaryIO, *values: Unpack[tuple[list[T]]]) -> Non
def unpack(self, buffer: ReadableBuffer) -> tuple[list[T]]:
header = self.header_serializer.unpack(buffer)
count, data_size = self._header_unpack_values(*header)
self.size = header_size = self.header_serializer.size
size = header_size = self.header_serializer.size
item_serializer = self.item_serializer.preunpack(self._partial_object)
items = []
for _ in range(count):
items.extend(self.item_serializer.unpack(buffer[self.size :]))
self.size += self.item_serializer.size
self._check_data_size(data_size, self.size - header_size)
items.extend(item_serializer.unpack(buffer[size :]))
size += item_serializer.size
self._check_data_size(data_size, size - header_size)
self.size = size
return (items,)

def unpack_from(self, buffer: ReadableBuffer, offset: int) -> tuple[list[T]]:
header = self.header_serializer.unpack_from(buffer, offset)
count, data_size = self._header_unpack_values(*header)
self.size = header_size = self.header_serializer.size
size = header_size = self.header_serializer.size
item_serializer = self.item_serializer.preunpack(self._partial_object)
items = []
for _ in range(count):
items.extend(self.item_serializer.unpack_from(buffer, offset + self.size))
self.size += self.item_serializer.size
self._check_data_size(data_size, self.size - header_size)
items.extend(item_serializer.unpack_from(buffer, offset + size))
size += item_serializer.size
self._check_data_size(data_size, size - header_size)
self.size = size
return (items,)

def unpack_read(self, readable: BinaryIO) -> tuple[list[T]]:
header = self.header_serializer.unpack_read(readable)
count, data_size = self._header_unpack_values(*header)
self.size = header_size = self.header_serializer.size
size = header_size = self.header_serializer.size
item_serializer = self.item_serializer.preunpack(self._partial_object)
items = []
for _ in range(count):
items.extend(self.item_serializer.unpack_read(readable))
self.size += self.item_serializer.size
self._check_data_size(data_size, self.size - header_size)
items.extend(item_serializer.unpack_read(readable))
size += item_serializer.size
self._check_data_size(data_size, size - header_size)
self.size = size
return (items,)


Expand Down
42 changes: 42 additions & 0 deletions structured/serializers/self.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
Serializer for special handling of the typing.Self typehint.
"""

__all__ = [
'SelfSerializer',
]


from ..type_checking import (
Any,
ClassVar,
TYPE_CHECKING,
annotated,
Self,
)
from .api import Serializer
from .structured import StructuredSerializer


if TYPE_CHECKING:
from ..structured import Structured, _Proxy
else:
Structured = 'Structured'
_Proxy = '_Proxy'

class SelfSerializer(Serializer[Structured]):
num_values: ClassVar[int] = 1

def prepack(self, partial_object: Structured) -> Serializer:
return StructuredSerializer(type(partial_object))

def preunpack(self, partial_object: _Proxy) -> Serializer:
return StructuredSerializer(partial_object.cls)

@classmethod
def _transform(cls, unwrapped: Any, actual: Any) -> Any:
if unwrapped is Self:
return cls()


annotated.register_transform(SelfSerializer._transform)
23 changes: 15 additions & 8 deletions structured/serializers/structured.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,32 +42,39 @@ class StructuredSerializer(Generic[TStructured], Serializer[TStructured]):

def __init__(self, obj_type: type[TStructured]) -> None:
self.obj_type = obj_type

@property
def size(self) -> int:
return self.obj_type.serializer.size
self.size = 0

def pack(self, values: TStructured) -> bytes:
return values.pack()
data = values.pack()
self.size = values.serializer.size
return data

def pack_into(
self, buffer: WritableBuffer, offset: int, values: TStructured
) -> None:
values.pack_into(buffer, offset)
self.size = values.serializer.size

def pack_write(self, writable: BinaryIO, values: TStructured) -> None:
values.pack_write(writable)
self.size = values.serializer.size

def unpack(self, buffer: ReadableBuffer) -> tuple[TStructured]:
return (self.obj_type.create_unpack(buffer),)
value = self.obj_type.create_unpack(buffer)
self.size = self.obj_type.serializer.size
return (value, )

def unpack_from(
self, buffer: ReadableBuffer, offset: int = 0
) -> tuple[TStructured]:
return (self.obj_type.create_unpack_from(buffer, offset),)
value = self.obj_type.create_unpack_from(buffer, offset)
self.size = self.obj_type.serializer.size
return (value, )

def unpack_read(self, readable: BinaryIO) -> tuple[TStructured]:
return (self.obj_type.create_unpack_read(readable),)
value = self.obj_type.create_unpack_read(readable)
self.size = self.obj_type.serializer.size
return (value, )

@classmethod
def _transform(cls, unwrapped: Any, actual: Any) -> Any:
Expand Down
104 changes: 78 additions & 26 deletions structured/serializers/unions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ClassVar,
Iterable,
ReadableBuffer,
WritableBuffer,
annotated,
get_union_args,
)
Expand Down Expand Up @@ -48,7 +49,7 @@ def __init__(self, result_map: dict[Any, Any], default: Any = None) -> None:
key: self.validate_serializer(serializer)
for key, serializer in result_map.items()
}
self._last_serializer = self.default
self.size = 0

@staticmethod
def validate_serializer(hint) -> Serializer:
Expand All @@ -59,16 +60,15 @@ def validate_serializer(hint) -> Serializer:
raise ValueError('Union results must serializer a single item.')
return serializer

@property
def size(self) -> int:
if self._last_serializer:
return self._last_serializer.size
else:
return 0
def prepack(self, partial_object) -> Serializer:
self._partial_object = partial_object
return self

def preunpack(self, partial_object) -> Serializer:
self._partial_object = partial_object
return self

def get_serializer(
self, decider_result: Any, partial_object: Any, packing: bool
) -> Serializer:
def get_serializer(self, decider_result: Any, packing: bool) -> Serializer:
"""Given a target used to decide, return a serializer used to unpack."""
if self.default is None:
try:
Expand All @@ -80,11 +80,9 @@ def get_serializer(
else:
serializer = self.result_map.get(decider_result, self.default)
if packing:
serializer = serializer.prepack(partial_object)
return serializer.prepack(self._partial_object)
else:
serializer = serializer.preunpack(partial_object)
self._last_serializer = serializer
return self._last_serializer
return serializer.preunpack(self._partial_object)

@staticmethod
def _transform(unwrapped: Any, actual: Any) -> Any:
Expand Down Expand Up @@ -120,13 +118,43 @@ def __init__(
super().__init__(result_map, default)
self.decider = decider

def prepack(self, partial_object: Any) -> Serializer:
result = self.decider(partial_object)
return self.get_serializer(result, partial_object, True)
def decide(self, packing: bool) -> Serializer:
result = self.decider(self._partial_object)
return self.get_serializer(result, packing)

def pack(self, *values: Any) -> bytes:
serializer = self.decide(True)
data = serializer.pack(*values)
self.size = serializer.size
return data

def pack_into(self, buffer: WritableBuffer, offset: int, *values: Any) -> None:
serializer = self.decide(True)
serializer.pack_into(buffer, offset, *values)
self.size = serializer.size

def pack_write(self, writable: BinaryIO, *values: Any) -> None:
serializer = self.decide(True)
serializer.pack_write(writable, *values)
self.size = serializer.size

def preunpack(self, partial_object: Any) -> Serializer:
result = self.decider(partial_object)
return self.get_serializer(result, partial_object, False)
def unpack(self, buffer: ReadableBuffer) -> Iterable:
serializer = self.decide(False)
value = serializer.unpack(buffer)
self.size = serializer.size
return value

def unpack_from(self, buffer: ReadableBuffer, offset: int = 0) -> Iterable:
serializer = self.decide(False)
value = serializer.unpack_from(buffer, offset)
self.size = serializer.size
return value

def unpack_read(self, readable: BinaryIO) -> Iterable:
serializer = self.decide(False)
value = serializer.unpack_read(readable)
self.size = serializer.size
return value


class LookaheadDecider(AUnion):
Expand All @@ -153,19 +181,43 @@ def __init__(
)
self.read_ahead_serializer = serializer

def prepack(self, partial_object: Any) -> Serializer:
result = self.decider(partial_object)
return self.get_serializer(result, partial_object, True)
def pack(self, *values: Any) -> bytes:
result = self.decider(self._partial_object)
serializer = self.get_serializer(result, True)
data = serializer.pack(*values)
self.size = serializer.size
return data

def pack_into(self, buffer: WritableBuffer, offset: int, *values: Any) -> None:
result = self.decider(self._partial_object)
serializer = self.get_serializer(result, True)
serializer.pack_into(buffer, offset, *values)
self.size = serializer.size

def pack_write(self, writable: BinaryIO, *values: Any) -> None:
result = self.decider(self._partial_object)
serializer = self.get_serializer(result, True)
serializer.pack_write(writable, *values)
self.size = serializer.size

def unpack(self, buffer: ReadableBuffer) -> Iterable:
result = tuple(self.read_ahead_serializer.unpack(buffer))[0]
return self.get_serializer(result, None, False).unpack(buffer)
serializer = self.get_serializer(result, False)
values = serializer.unpack(buffer)
self.size = serializer.size
return values

def unpack_from(self, buffer: ReadableBuffer, offset: int = 0) -> Iterable:
result = tuple(self.read_ahead_serializer.unpack_from(buffer, offset))[0]
return self.get_serializer(result, None, False).unpack_from(buffer, offset)
serializer = self.get_serializer(result, False)
values = serializer.unpack_from(buffer, offset)
self.size = serializer.size
return values

def unpack_read(self, readable: BinaryIO) -> Iterable:
result = tuple(self.read_ahead_serializer.unpack_read(readable))[0]
readable.seek(-self.read_ahead_serializer.size, os.SEEK_CUR)
return self.get_serializer(result, None, False).unpack_read(readable)
serializer = self.get_serializer(result, False)
values = serializer.unpack_read(readable)
self.size = serializer.size
return values
Loading

0 comments on commit e5d3f9f

Please sign in to comment.