Skip to content
This repository has been archived by the owner on Feb 5, 2024. It is now read-only.

Repeating Kwargifier. #35

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
150 changes: 117 additions & 33 deletions bytestring_splitter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class PartiallySplitBytes:
"""
Represents a bytestring which has been split but not instantiated as processed objects yet.
"""

def __init__(self, processed_objects):
self.processed_objects = processed_objects

Expand Down Expand Up @@ -51,9 +52,9 @@ def finish(self):

for message_name, (bytes_for_message, message_class, kwargs) in self.processed_objects.items():
self._finished_values[message_name] = produce_value(message_class,
message_name,
bytes_for_message,
kwargs)
message_name,
bytes_for_message,
kwargs)
return self._receiver(**self._finished_values, **self._additional_kwargs)

def __getattr__(self, message_name):
Expand All @@ -66,15 +67,15 @@ def __getattr__(self, message_name):
try:
bytes_for_message, message_class, kwargs = self.processed_objects[message_name]
self._finished_values[message_name] = produce_value(message_class,
message_name,
bytes_for_message,
kwargs)
message_name,
bytes_for_message,
kwargs)
produced_value = produce_value(message_class, message_name, bytes_for_message, kwargs)
del self.processed_objects[message_name] # We don't do this as a pop() in case produce_value raises.
return produced_value
except KeyError:
raise AttributeError(f"{self.__class__} doesn't have a {message_name}, and it's not a partially split object either; those are {list(self.processed_objects.keys())}")

raise AttributeError(
f"{self.__class__} doesn't have a {message_name}, and it's not a partially split object either; those are {list(self.processed_objects.keys())}")

def __bytes__(self):
return self._original_bytes
Expand Down Expand Up @@ -153,6 +154,28 @@ def __call__(self, splittable: bytes,
or raise an error if there is a remainder.
:return: Either a collection of objects of the types specified in message_types or, if single, a single object.
"""
processed_objects, remainder = self.actually_split(splittable, return_remainder, msgpack_remainder, partial,
single)
processed_objects = self.deal_with_remainder(processed_objects, remainder, msgpack_remainder=msgpack_remainder,
return_remainder=return_remainder)

if partial:
return self.partial_receiver(processed_objects)
return processed_objects

def deal_with_remainder(self, processed_objects, remainder, msgpack_remainder=False, return_remainder=False):
if msgpack_remainder:
try:
import msgpack
except ImportError:
raise RuntimeError("You need to install msgpack to use msgpack_remainder.")
# TODO: Again, not very agnostic re: collection type here.
processed_objects.append(msgpack.loads(remainder))
elif return_remainder:
processed_objects.append(remainder)
return processed_objects

def actually_split(self, splittable, return_remainder, msgpack_remainder, partial, single):
if not self.is_variable_length:
if not (return_remainder or msgpack_remainder) and len(self) != len(splittable):
message = "Wrong number of bytes to constitute message types {} - need {}, got {} Did you mean to return the remainder?"
Expand Down Expand Up @@ -201,8 +224,9 @@ def __call__(self, splittable: bytes,
if single:
_remainder = len(splittable[cursor:])
if _remainder:
raise ValueError(f"The bytes don't represent a single {message_class}; there are {_remainder} too many.") # TODO
return value
raise ValueError(
f"The bytes don't represent a single {message_class}; there are {_remainder} too many.") # TODO
return value, False
else:
try: # TODO: Make this more agnostic toward the collection type.
processed_objects[message_name] = value
Expand All @@ -211,19 +235,7 @@ def __call__(self, splittable: bytes,

remainder = splittable[cursor:]

if msgpack_remainder:
try:
import msgpack
except ImportError:
raise RuntimeError("You need to install msgpack to use msgpack_remainder.")
# TODO: Again, not very agnostic re: collection type here.
processed_objects.append(msgpack.loads(remainder))
elif return_remainder:
processed_objects.append(remainder)

if partial:
return self.partial_receiver(processed_objects)
return processed_objects
return processed_objects, remainder

def __len__(self):
return self._length
Expand Down Expand Up @@ -355,28 +367,78 @@ def __init__(self, _receiver=None, _partial_receiver=None, _additional_kwargs=No
self._additional_kwargs = _additional_kwargs or {}
super().__init__(*parameter_pairs.items())

def __call__(self, splittable, receiver=None, partial=False, *args, **kwargs):
def __call__(self, splittable, receiver=None, partial=False, return_remainder=False, *args, **kwargs):
receiver = receiver or self.receiver

if receiver is None:
raise TypeError(
"Can't fabricate without a receiver. You can either pass one when calling or pass one when init'ing.")
"Can't kwargify without a receiver. You can either pass one when calling or pass one when init'ing.")

result = super().__call__(splittable, partial=partial, *args, **kwargs)
if partial:
result.set_receiver(receiver)
result.set_additional_kwargs(self._additional_kwargs)
result.set_original_bytes_repr(splittable)
return result
container = []

while True:
if return_remainder:
result, remainder = BytestringSplitter.__call__(self, splittable, partial=partial,
return_remainder=return_remainder, *args, **kwargs)
else:
result = BytestringSplitter.__call__(self, splittable, partial=partial,
return_remainder=return_remainder, *args, **kwargs)
remainder = None

if partial:
result.set_receiver(receiver)
result.set_additional_kwargs(self._additional_kwargs)
result.set_original_bytes_repr(splittable)
container.append(result)
else:
container.append(receiver(**result, **self._additional_kwargs))
if not remainder:
break
else:
splittable = remainder

if len(container) == 1:
return container[0]
else:
return receiver(**result, **self._additional_kwargs)
return container

@staticmethod
def _parse_message_meta(message_item):
message_name, message_type = message_item
_, message_class, message_length, kwargs = BytestringSplitter._parse_message_meta(message_type)
return BytestringSplitter.Message(message_name, message_class, message_length, kwargs)

def deal_with_remainder(self, processed_objects, remainder, msgpack_remainder=False, return_remainder=False):
if remainder:
_processed_objects = [processed_objects]
if msgpack_remainder:
try:
import msgpack
except ImportError:
raise RuntimeError("You need to install msgpack to use msgpack_remainder.")
# TODO: Again, not very agnostic re: collection type here.
_processed_objects.append(msgpack.loads(remainder))
elif return_remainder:
_processed_objects.append(remainder)
else:
raise BytestringSplittingError("Kwargifier sees a remainder, but return_remainder is False.")
return _processed_objects
elif return_remainder:
return processed_objects, False
else:
return processed_objects

def repeat(self, splittable, as_set=False):
"""
Continue to split the splittable until we get to the end.

If as_set, return values as a set rather than a list.
"""
if as_set:
raise BytestringSplittingError("Don't try to kwargify with as_set - we're not going to check if your objects are hashable.")
messages = self(splittable, return_remainder=True)
return messages


class HeaderMetaDataMixinBase:
"""
Expand Down Expand Up @@ -573,6 +635,27 @@ def validate_checksum(self, some_bytes, raise_exception=False):
class VersionedBytestringSplitter(VersioningMixin, BytestringSplitter):
pass

def repeat(self, splittable, as_set=False):
"""
Continue to split the splittable until we get to the end.

If as_set, return values as a set rather than a list.
"""
remainder = True
if as_set:
messages = set()
collector = messages.add
else:
messages = []
collector = messages.append
while remainder:
*message, remainder = self(splittable, return_remainder=True)
if len(message) == 1:
message = message[0]
collector(message)
splittable = remainder
return messages


class VersionedBytestringKwargifier(VersionedBytestringSplitter, BytestringKwargifier):
"""
Expand Down Expand Up @@ -643,7 +726,8 @@ def dispense(bytestring):
message_length = int.from_bytes(message_length_as_bytes, "big")
message = bytestring[VARIABLE_HEADER_LENGTH:]
if not message_length == len(message):
raise BytestringSplittingError("This does not appear to be a VariableLengthBytestring, or is not the correct length.")
raise BytestringSplittingError(
"This does not appear to be a VariableLengthBytestring, or is not the correct length.")

try:
items = BytestringSplitter(VariableLengthBytestring).repeat(message)
Expand Down
12 changes: 12 additions & 0 deletions tests/test_kwargifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ def test_kwargified_coffee():
assert cup_of_coffee.size == 54453


def test_kwargified_several_cups():
first_cup = VariableLengthBytestring(b"Equal Exchange Mind, Body, and Soul") + b"local_oatmilk" + int(
54453).to_bytes(2, byteorder="big")
second_cup = VariableLengthBytestring(b"Sandino Roasters Blend") + b"half_and_half" + int(16).to_bytes(2,
byteorder="big")
cups_as_bytes = first_cup + second_cup
two_cups = coffee_splitter.repeat(cups_as_bytes)

assert two_cups[0].blend == b"Equal Exchange Mind, Body, and Soul"
assert two_cups[1].blend == b"Sandino Roasters Blend"


def test_partial_instantiation():
coffee_as_bytes = VariableLengthBytestring(b"Sandino Roasters Blend") + b"half_and_half" + int(16).to_bytes(2, byteorder="big")

Expand Down
5 changes: 4 additions & 1 deletion tests/test_splitters.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@

import msgpack
import pytest

from bytestring_splitter import BytestringSplitter, VariableLengthBytestring, BytestringSplittingError
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring, BytestringSplittingError, \
BytestringKwargifier, VersionedBytestringKwargifier


def test_splitting_one_message():
Expand Down Expand Up @@ -217,6 +219,7 @@ def from_bytes(cls, thing_as_bytes, necessary_kwarg=False):

assert result[0].what_it_be == things_as_bytes[:35]


def test_bundle_and_dispense_variable_length():
items = [b'llamas', b'dingos', b'christmas-tree']
vbytes = bytes(VariableLengthBytestring.bundle(items))
Expand Down