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.1.0] - 2025-09-09
### Added
- The `Bytes.encode()` and the `Bytes.decode()` methods now support reversing the order
of the provided bytes via the `reverse` boolean keyword argument; by default bytes are
held and encoded in the order in which they are provided; if the `reverse` argument is
set to `True`, the order of will be reversed; the byte `order` argument has no impact on
the byte ordering of the data, as the `Bytes` type and its subtypes simply hold one or
more individual 8-bit byte values, which are meant to be unaffected by byte order. The
`reverse` argument can be used to reverse the ordering if needed.
- The new `isinstantiable()` utility function can be used to determine if a value can be
instantiated into the specified class type determined based on data type compatibility.
- Documentation for the utility functions in the `deliciousbytes.utilities` submodule.
- Unit testing for the utility functions in the `deliciousbytes.utilities` submodule.

## [1.0.4] - 2025-08-01
### Added
- The `Bytes.encode()` method now pads the generated bytes to the specified length, if a
Expand Down
105 changes: 102 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ assert encoded == b"\x7f"
The DeliciousBytes library provides the following data type classes, all of which are
ultimately a subclass of one of the native Python data type classes so all instances of
the classes below can be used interchangeably with their native Python data types; each
data type class is also a subclasses of the `deliciousbytes.Type` superclass, from which
data type class is also a subclass of the `deliciousbytes.Type` superclass, from which
they inherit shared behaviour and class hierarchy membership:

| Class | Description | Subclass Of | Format |
Expand Down Expand Up @@ -153,6 +153,16 @@ its native data type value. The byte order defaults to most-significant bit firs
is represented by the `ByteOrder` enumeration class which provides enumeration options
to specify the endianness that is needed for the use case.

Furthermore, the `Bytes` type and it subtypes, `Bytes8`, `Bytes16`, `Bytes32`, `Bytes64`,
`Bytes128`, and `Bytes256` offer a `reverse` (`bool`) keyword argument that can be used to
reverse the order of bytes from those provided, as in the case of the `Bytes` type and
its subtypes, the classes expect to hold one or more individual bytes, were the order
of the bytes is not expected to affect the encoding of the underlying data, and as such
the value of the 'order' keyword argument, has no impact. Where it is useful to reverse
the order of the bytes being held, the `reverse` keyword argument can be set to `True`,
causing the order of the individual bytes to be reversed into the opposite order to that
in which they were provided.

The `deliciousbytes` class also provides a `ByteView` class which provides a method for
iterating over bytes, either as individual bytes or in groups of bytes of the specified
split size, where these bytes may be accessed as raw `bytes` values or cast into one of
Expand Down Expand Up @@ -286,13 +296,102 @@ assert view.decode(">hhh") == (1, 2, 3)
assert view.decode(">hhhh") == (1, 2, 3, 4)
```

<a id='utility-functions'></a>
### Utility Functions

The DeliciousBytes library provides the following utility functions which are useful for
debugging and integration:

* `hexbytes(value: bytes, prefix: bool = False, limit: int = 0)` (`str`) – The `hexbytes()`
function takes a `bytes` value as input and generates a string representation of the value,
that can be printed or stored and later reviewed. The output is primarily useful for
debugging purposes to help visualise raw byte data.


An example of the `hexbytes()` function's use is as follows:

```python
from deliciousbytes.utilities import hexbytes

value: bytes = b"\x01\x02\x03\x04\x05\x06"

assert hexbytes(value) == "[> 01 02 03 04 05 06 <]"
```

The optional `prefix` option changes the output to look like a formatted bytes string:

```python
from deliciousbytes.utilities import hexbytes

value: bytes = b"\x01\x02\x03\x04\x05\x06"

assert hexbytes(value, prefix=True) == r'b"\x01\x02\x03\x04\x05\x06"'
```

The optional `limit` option, limits how many bytes are included in the output:

```python
from deliciousbytes.utilities import hexbytes

value: bytes = b"\x01\x02\x03\x04\x05\x06"

assert hexbytes(value, limit=4) == "[> 01 02 03 04 ... <]"
```

* `print_hexbytes` – The `print_hexbytes()` function takes a `bytes` value as input and
generates and prints a string representation of the value. The output is primarily useful for
debugging purposes to help visualise raw byte data.

An example of the `print_hexbytes()` function's use is as follows:

```python
from deliciousbytes.utilities import print_hexbytes

value: bytes = b"\x01\x02\x03\x04\x05\x06"

print_hexbytes(value, limit=4)
```

The above code outputs the following:

```text
[> 01 02 03 04 ... <]
```

* `isinstantiable(value: object, klass: type)` (`bool`) – The `isinstantiable()`
function takes an `object` value, of any class, and a type class reference, and based
on the class type or superclass types of the type class reference, determines if the
value can be instantiated into an instance of the referenced type class. For example,
if a string value is provided, and the type class reference references a class that is
a subclass of the `str` type, then the function will return `True` otherwise `False`.

An example of the `isinstantiable()` function's use is as follows:

```python
from deliciousbytes import String, ASCII, UTF8
from deliciousbytes.utilities import isinstantiable

value: str = "Hello World"

assert isinstantiable(value, str)
assert isinstantiable(value, String)
assert isinstantiable(value, ASCII)
assert isinstantiable(value, UTF8)
```

The above code outputs the following:

```text
[> 01 02 03 04 ... <]
```

<a id='byte-order'></a>
### Byte Order

The byte order for each of the data type classes defaults to most-significant bit first,
MSB, but may be changed to least-significant bit first, LSB, if needed. The `ByteOrder`
enumeration class value offers enumeration options to specify the endianness that is
needed for the use case, and for convenience provides the enumerations in a few flavours
enumeration class value offers enumeration options to specify the endianness needed for
a given use case, and for convenience provides the enumerations in a few naming flavours
depending on how one prefers to refer to endianness:

| Enumeration Option | Byte Order | Endianness |
Expand Down
45 changes: 40 additions & 5 deletions source/deliciousbytes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -998,8 +998,20 @@ def __new__(cls, value: bytes | bytearray | Int, length: int = None):
return self

def encode(
self, order: ByteOrder = ByteOrder.MSB, length: int = None, raises: bool = True
self,
order: ByteOrder = ByteOrder.MSB,
reverse: bool = False,
length: int = None,
raises: bool = True,
) -> bytes:
"""The encode method encodes the provided bytes into a bytes type, padding the
value up to the specified length. The byte order is ignored as the Bytes type
holds one of more individual bytes, similar to the ASCII type, where the values
being encoded already fit within single bytes so byte order has no impact. If
there is the need to reverse the order of the bytes, this can be achieved via
the 'reverse' argument, which defaults to False, but can be set to True to sort
and encode the bytes in reverse order."""

if not isinstance(self, Bytes):
raise TypeError(
"Ensure the 'encode' method is being called on a class instance!"
Expand All @@ -1010,6 +1022,9 @@ def encode(
"The 'order' argument must have a ByteOrder enumeration value!"
)

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

if length is None:
length = self._length
elif not (isinstance(length, int) and length >= 1):
Expand All @@ -1022,14 +1037,14 @@ def encode(

encoded: bytesarray = bytearray()

if order is ByteOrder.MSB:
if reverse is False:
for index, byte in enumerate(self):
# logger.debug("%s.encode(order: MSB) index => %s, byte => %s (%x)", self.__class__.__name__, index, byte, byte)
encoded.append(byte)

while length > 0 and len(encoded) < length:
encoded.insert(0, 0)
elif order is ByteOrder.LSB:
elif reverse is True:
for index, byte in enumerate(reversed(self)):
# logger.debug("%s.encode(order: LSB) index => %s, byte => %s (%x)", self.__class__.__name__, index, byte, byte)
encoded.append(byte)
Expand All @@ -1046,13 +1061,33 @@ def encode(
return bytes(encoded)

@classmethod
def decode(cls, value: bytes | bytearray, order: ByteOrder = None) -> Bytes:
def decode(
cls,
value: bytes | bytearray,
order: ByteOrder = ByteOrder.MSB,
reverse: bool = False,
) -> Bytes:
"""The decode method decodes the provided value into a Bytes type; the byte
order is ignored as the Bytes type holds one of more individual bytes, similar
to the ASCII type, where the values being encoded already fit within single
bytes so byte order has no impact. If there is the need to reverse the order of
the bytes, this can be achieved via the 'reverse' argument, which defaults to
False, but can be set to True to sort and decode the bytes in reverse order."""

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

if order is ByteOrder.LSB:
if not isinstance(order, ByteOrder):
raise TypeError(
"The 'order' argument must have a ByteOrder enumeration value!"
)

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

if reverse is True:
value = bytes(reversed(value))

return cls(value=bytes(value))
Expand Down
24 changes: 23 additions & 1 deletion source/deliciousbytes/utilities/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import builtins


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 hasattr(data, "__bytes__"):
data = bytes(data)
else:
raise TypeError(
"The 'data' argument must have a bytes or bytesarray value, not %s!"
% (type(data),)
)

if not isinstance(prefix, bool):
raise TypeError("The 'prefix' argument must have a boolean value!")
Expand All @@ -28,3 +37,16 @@ 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))


def isinstantiable(value: object, klass: type) -> bool:
"""Determine if the value can be instantiated as an instance of the noted class."""

if not isinstance(value, object):
raise TypeError("The 'value' argument must have an object value!")

if not isinstance(klass, type):
raise TypeError("The 'klass' argument must have a type value!")

# Determine if the value type class appears in the base classes of the noted class:
return builtins.type(value) in klass.mro()
2 changes: 1 addition & 1 deletion source/deliciousbytes/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.4
1.1.0
Loading