Skip to content
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# DeliciousBytes Library Change Log

## [1.0.3] - 2025-07-07
### Added
- Input validation for the `.encode()` methods.
- Additional unit testing for the `UnsignedLong` type.
- A new `utilities` sub-module containing the `hexbytes` helper method that can be used
to format raw `bytes` or `bytearray` values into hexadecimal encoded strings for review,
and the `print_hexbytes` helper method that can be used to print such strings.

### Changed
- Type casting of the decoded return value for the `Bytes` class when reversed back to a
`bytes` type, fixing a previous issue where the reversed return type was a `list` type.
- The `BytesView` class `split` length can now be equal to the length of the `data`.
- Corrected type hints for the `BytesView` class' `__iter__` and `__next__` methods.

## [1.0.2] - 2025-06-17
### Added
- Added support for creating `Bytes` values from `bytes` values.
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ services:
build:
dockerfile: ./Dockerfile
context: ./
image: xml-black
image: deliciousbytes-black
environment:
- SERVICE=black
volumes:
Expand All @@ -22,7 +22,7 @@ services:
build:
dockerfile: ./Dockerfile
context: ./
image: xml-tests
image: deliciousbytes-tests
environment:
- SERVICE=tests
volumes:
Expand Down
47 changes: 40 additions & 7 deletions source/deliciousbytes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from classicist import classproperty

from deliciousbytes.utilities import hexbytes

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -337,6 +338,11 @@ def MAX(cls) -> int:
return cls._maximum

def encode(self, order: ByteOrder = None) -> bytes:
if not isinstance(self, Int):
raise TypeError(
"Ensure the 'encode' method is being called on a class instance!"
)

if order is None:
order = self.order
elif not isinstance(order, ByteOrder):
Expand All @@ -346,7 +352,9 @@ def encode(self, order: ByteOrder = None) -> bytes:

if self.length > 0:
return self.to_bytes(
length=self.length, byteorder=order.value, signed=self.signed
length=self.length,
byteorder=order.value,
signed=self.signed,
)
else:
return self.to_bytes(
Expand All @@ -372,7 +380,11 @@ def decode(cls, value: bytes | bytearray, order: ByteOrder = ByteOrder.MSB) -> I
)

logger.debug(
"%s.decode(value: %r, order: %r) => %r", cls.__name__, value, order, decoded
"%s.decode(value: %r, order: %r) => %r",
cls.__name__,
hexbytes(value),
order,
decoded,
)

return decoded
Expand Down Expand Up @@ -892,6 +904,11 @@ def MAX(cls) -> float:
return cls._maximum

def encode(self, order: ByteOrder = ByteOrder.MSB) -> bytes:
if not isinstance(self, Float):
raise TypeError(
"Ensure the 'encode' method is being called on a class instance!"
)

if not isinstance(order, ByteOrder):
raise TypeError(
"The 'order' argument must reference a ByteOrder enumeration option!"
Expand Down Expand Up @@ -982,6 +999,11 @@ def __new__(cls, value: bytes | bytearray | Int, length: int = None):
def encode(
self, order: ByteOrder = ByteOrder.MSB, length: int = None, raises: bool = True
) -> bytes:
if not isinstance(self, Bytes):
raise TypeError(
"Ensure the 'encode' method is being called on a class instance!"
)

if not isinstance(order, ByteOrder):
raise TypeError(
"The 'order' argument must have a ByteOrder enumeration value!"
Expand Down Expand Up @@ -1030,7 +1052,7 @@ def decode(cls, value: bytes | bytearray, order: ByteOrder = None) -> Bytes:
)

if order is ByteOrder.LSB:
value = reversed(value)
value = bytes(reversed(value))

return cls(value=bytes(value))

Expand Down Expand Up @@ -1076,6 +1098,11 @@ def encode(
order: ByteOrder = ByteOrder.MSB,
encoding: Encoding = None,
):
if not isinstance(self, String):
raise TypeError(
"Ensure the 'encode' method is being called on a class instance!"
)

if encoding is None:
encoding = self.encoding
elif not isinstance(encoding, Encoding):
Expand Down Expand Up @@ -1201,7 +1228,7 @@ def __init__(

if not isinstance(split, int):
raise TypeError("The 'split' argument must have a positive integer value!")
elif 1 <= split < self._size:
elif 1 <= split <= self._size:
self._splits = split
else:
raise ValueError(
Expand All @@ -1221,11 +1248,11 @@ def __len__(self) -> int:
else:
return math.floor(parts)

def __iter__(self) -> bytesview:
def __iter__(self) -> BytesView:
self._index = 0
return self

def __next__(self) -> bytes | object:
def __next__(self) -> bytearray | object:
if self._index + self._splits > self._size:
raise StopIteration

Expand All @@ -1242,7 +1269,7 @@ def __next__(self) -> bytes | object:
return value

def __getitem__(self, index: int | slice) -> bytearray | object:
# logger.debug("%s.__getitem__(index: %r)" % (self.__class__.__name__, index))
logger.debug("%s.__getitem__(index: %r)", self.__class__.__name__, index)

maxindex: int = math.floor(self._size / self._splits) - 1
reverse: bool = False
Expand Down Expand Up @@ -1397,6 +1424,7 @@ def typed(self) -> type | None:
self.type,
typed,
)

return typed

def split(self, split: int = None) -> BytesView:
Expand Down Expand Up @@ -1530,6 +1558,11 @@ def decode(
in the BytesView, but if the format string specifies more data types than held
in the data, an error will be raised."""

if not isinstance(self, BytesView):
raise TypeError(
"Ensure the 'decode' method is being called on a class instance!"
)

if not isinstance(format, str):
raise TypeError("The 'format' argument must have a string value!")
elif not len(format := format.strip()) > 0:
Expand Down
30 changes: 30 additions & 0 deletions source/deliciousbytes/utilities/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
def hexbytes(data: bytes | bytearray, prefix: bool = False, limit: int = 0) -> str:
"""Format a bytes or bytearray value into a human readable string for debugging."""

if not isinstance(data, (bytes, bytearray)):
raise TypeError("The 'data' argument must have a bytes or bytesarray value!")

if not isinstance(prefix, bool):
raise TypeError("The 'prefix' argument must have a boolean value!")

if not (isinstance(limit, int) and limit >= 0):
raise TypeError("The 'limit' argument must have a positive integer value!")

hex_string = ("" if prefix else " ").join(
[
(r"\x" if prefix else "") + f"{byte:02x}"
for (index, byte) in enumerate(data)
if limit == 0 or index < limit
]
)

if limit > 0 and len(data) > limit:
hex_string += " ..."

return ('b"' if prefix else "[> ") + hex_string + ('"' if prefix else " <]")


def print_hexbytes(data: bytes | bytearray, **kwargs) -> None:
"""Print a bytes or bytearray value as a human readable string for debugging."""

print(hexbytes(data=data, **kwargs))
2 changes: 1 addition & 1 deletion source/deliciousbytes/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.2
1.0.3
7 changes: 0 additions & 7 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,3 @@
sys.path.insert(0, path) # add library path for importing into the tests

import deliciousbytes


def print_hexbytes(data: bytes, prefix: bool = True):
hex_string = ("" if prefix else " ").join(
[(r"\x" if prefix else "") + f"{byte:02x}" for byte in data]
)
print(('b"' if prefix else "") + hex_string + ('"' if prefix else ""))
24 changes: 23 additions & 1 deletion tests/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
ASCII,
)

from conftest import print_hexbytes
from deliciousbytes.utilities import print_hexbytes


def test_byte_order_enumeration():
Expand Down Expand Up @@ -519,6 +519,28 @@ def test_unsigned_long():
assert isinstance(encoded, bytes)
assert encoded == b"\x7f\x00\x00\x00"

# 5848 in big endian is \x16\xd8
# 5848 in litte endian is \xd8\x16

data: Bytes = Bytes.decode(b"\xd8\x16", order=ByteOrder.LSB)
assert isinstance(data, Bytes)
assert data == b"\x16\xd8" # Bytes.decode() flips the bytes to MSB order

decoded: UInt32 = UInt32.decode(data)
assert isinstance(decoded, UInt32)
assert isinstance(decoded, int)
assert decoded == 5848

decoded: UInt64 = UInt64.decode(data)
assert isinstance(decoded, UInt64)
assert isinstance(decoded, int)
assert decoded == 5848

decoded: UnsignedLong = UnsignedLong.decode(data)
assert isinstance(decoded, UnsignedLong)
assert isinstance(decoded, int)
assert decoded == 5848


def test_signed_long():
"""Test the SignedLong data type which is a fixed 4-byte, 32-bit signed integer type."""
Expand Down