diff --git a/CHANGELOG.md b/CHANGELOG.md
index f93b772..b9ced94 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,24 @@
# DeliciousBytes Library Change Log
+## [1.0.2] - 2025-06-17
+### Added
+- Added support for creating `Bytes` values from `bytes` values.
+- Added a new `BytesView` class for iterating over `bytes` and `bytearray` objects with
+the ability to access the data as arbitrarily sized groups of bytes as well as being able
+to cast bytes to specific data types.
+- Added `Float`, `Float16`, `Float32` and `Float64` types, as well as `Double` (an alias for `Float64`).
+- Added `Size`, `SingedSize` and `UnsignedSize` integer subtypes which have a maximum size dependent upon the system they are running on.
+- Added `Unicode`, `UTF8`, `UTF16`, `UTF32`, `ASCII` types to compliment the `String` type; these string variants hold strings with different default character encodings.
+- Added `Type` superclass as a parent of all of the type subclasses which makes type comparison and class hierarchy membership easier to discern and allows shared behaviour to be centrally defined and maintained.
+
+### Changed
+- The `Short` type was previously unsigned and is now signed as per the C standard following the Python convention; the `Short` type was previously based on embedded metadata standards, which treat `short` as unsigned and defined a separate `signed short`.
+- The `Long` type was previously unsigned and is now signed as per the C standard following the Python convention; the `Long` type was previously based on embedded metadata standards, which treat `long` as unsigned and defined a separate `signed long`.
+- The `LongLong` type was previously unsigned and is now signed as per the C standard following the Python convention; the `LongLong` type was previously based on embedded metadata standards, which treat `long long` as unsigned and defined a separate `signed long long`.
+- A new `UnsignedShort` type has been added to compliment the signed `Short` type and pairs with the `SignedShort` type which is functionally equivalent to `Short`.
+- A new `UnsignedLong` type has been added to compliment signed `Long` type and pairs with the `SignedLong` type which is functionally equivalent to `Long`.
+- A new `UnsignedLongLong` type has been added to compliment signed `LongLong` type and pairs with the `SignedLongLong` type which is functionally equivalent to `LongLong`.
+
## [1.0.1] - 2025-06-11
### Added
- Improved input validation for `String.encode()` and `String.decode()`.
diff --git a/Dockerfile b/Dockerfile
index 13cfb90..405886d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,7 +3,7 @@
################################# [Base Python Image] ##################################
# Allow the Python version to be specified as a build argument, with a preferred default
-ARG VERSION=3.12
+ARG VERSION=3.13
FROM python:${VERSION} AS base
diff --git a/README.md b/README.md
index e4ce942..2940773 100644
--- a/README.md
+++ b/README.md
@@ -5,12 +5,13 @@ decoded from their binary forms which is useful when working with data that is s
certain file formats or transmitted over networks with certain encodings.
The data types provided by the library all subclass their corresponding native Python
-types so can be used interchangeably with those types, while offering additional support
+types so can be used interchangeably with those types while offering standardised support
for decoding from and encoding into binary forms according to the specified byte order.
-The library provides a range of signed and unsigned integer types of specific lengths,
-chars, signed chars, signed and unsigned longs, signed and unsigned long longs, bytes
-and string types.
+The library provides a range of signed and unsigned integer types of fixed lengths,
+`char`, unsigned `char`, equivalents for signed and unsigned `short`, signed and unsigned
+`long`, signed and unsigned `long long`, as well as `float`, `double`, and `bytes` and
+various string types of different default character encodings.
The integer types automatically overflow if the specified value is out of range, for
example if the unsigned 8-bit integer type, `UInt8`, which can hold `255` as its largest
@@ -18,9 +19,9 @@ value, is instantiated with a value of `256` it will automatically overflow to `
if a signed 8-bit integer type, `Int8`, which can hold a minimum value of `-127` and a
maximum value of `128` is instantiated with a value of `129` it will overflow to `-127`.
-While many of the built in types offer conversion operations to and from their binary
-forms, the library provides a consistent interface across the data types and also offers
-the ability to encode and decode bytes and string values with a defined endianness.
+While many of the built-in types offer conversion operations to and from their binary
+forms, the library provides a consistent interface across the data types to achieve this
+as well as the encoding and decoding of bytes and string values with defined endianness.
### Requirements
@@ -39,9 +40,9 @@ using `pip` via the `pip install` command by entering the following into your sh
### Example Usage
To use the DeliciousBytes library, import the library and the data type or data types
-you need and use them just like their regular counterparts, and when needed the each
-types' `encode()` and `decode()` methods provide support for decoding and encoding the
-values to and from their binary representations:
+one needs for a given project and use them just like their regular counterparts. When
+required, each of the types' `encode()` and `decode()` methods provide support for
+encoding and decoding the values to and from their binary representations:
```python
from deliciousbytes import (
@@ -50,9 +51,9 @@ from deliciousbytes import (
value: Int8 = Int8(127)
-assert isinstance(value, int)
-assert isinstance(value, Int)
assert isinstance(value, Int8)
+assert isinstance(value, Int)
+assert isinstance(value, int)
assert value == 127
@@ -65,42 +66,73 @@ assert isinstance(encoded, bytes)
assert encoded == b"\x7f"
```
+
### Classes & Methods
-The DeliciousBytes library provides the following data type classes:
-
-| Class | Description | Subclass Of |
-|------------------|-------------------------------------|-------------|
-| `Int` | Signed unbounded integer | `int` |
-| `Int8` | Signed 8-bit integer | `Int` |
-| `Int16` | Signed 16-bit integer | `Int` |
-| `Int32` | Signed 32-bit integer | `Int` |
-| `Int64` | Signed 64-bit integer | `Int` |
-| `UInt` | Unsigned unbounded integer | `Int` |
-| `UInt8` | Unsigned 8-bit integer | `UInt` |
-| `UInt16` | Unsigned 16-bit integer | `UInt` |
-| `UInt32` | Unsigned 32-bit integer | `UInt` |
-| `UInt64` | Unsigned 64-bit integer | `UInt` |
-| `Char` | Unsigned 8-bit integer | `UInt8` |
-| `SignedChar` | Signed 8-bit integer | `Int8` |
-| `Long` | Unsigned long (16-bit) integer | `UInt16` |
-| `SignedLong` | Signed long (16-bit) integer | `Int16` |
-| `LongLong` | Unsigned long long (32-bit) integer | `UInt32` |
-| `SignedLongLong` | Signed long long (32-bit) integer | `Int32` |
-| `Bytes` | Unbounded bytes type | `bytes` |
-| `Bytes8` | 8-bit bytes type | `Bytes` |
-| `Bytes16` | 16-bit bytes type | `Bytes` |
-| `Bytes32` | 32-bit bytes type | `Bytes` |
-| `Bytes64` | 64-bit bytes type | `Bytes` |
-| `Bytes128` | 128-bit bytes type | `Bytes` |
-| `Bytes256` | 256-bit bytes type | `Bytes` |
-| `String` | Unbounded string type | `str` |
+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
+they inherit shared behaviour and class hierarchy membership:
+
+| Class | Description | Subclass Of | Format |
+|--------------------|----------------------------------------|-------------|:--------:|
+| `Int` | Signed unbounded integer | `int` | |
+| `Int8` | Signed 8-bit integer | `Int` | |
+| `Int16` | Signed 16-bit integer | `Int` | |
+| `Int32` | Signed 32-bit integer | `Int` | |
+| `Int64` | Signed 64-bit integer | `Int` | |
+| `UInt` | Unsigned unbounded integer | `Int` | |
+| `UInt8` | Unsigned 8-bit integer | `UInt` | |
+| `UInt16` | Unsigned 16-bit integer | `UInt` | |
+| `UInt32` | Unsigned 32-bit integer | `UInt` | |
+| `UInt64` | Unsigned 64-bit integer | `UInt` | |
+| `Char` | An 8-bit integer, defaults to unsigned | `UInt8` | `c` |
+| `SignedChar` | Signed 8-bit integer | `Int8` | `b` |
+| `UnsignedChar` | Unsigned 8-bit integer | `UInt8` | `B` |
+| `Short` | Signed short (16-bit) integer | `Int16` | `h` |
+| `SignedShort` | An alias for `Short` | `Short` | `h` |
+| `UnsignedShort` | Unsigned short (16-bit) integer | `UInt16` | `H` |
+| `Long` | Signed long (32-bit) integer | `Int32` | `l` |
+| `SignedLong` | An alias for `Long` | `Long` | `l` |
+| `UnsignedLong` | Unsigned long (32-bit) integer | `UInt32` | `L` |
+| `LongLong` | Signed long long (64-bit) integer | `Int64` | `q` |
+| `SignedLongLong` | An alias for `LongLong` | `LongLong` | `q` |
+| `UnsignedLongLong` | Unsigned long long (64-bit) integer | `UInt64` | `Q` |
+| `Size` | A unsigned integer type of system size | `UInt` | `n` |
+| `SignedSize` | A signed integer type of system size | `Int` | `N` |
+| `UnsignedSize` | An alias for `Size` | `Size` | `n` |
+| `Float` | A float type, defaulting to 64-bit | `float` | `d` |
+| `Float16` | 16-bit float type | `Float` | `e` |
+| `Float32` | 32-bit float type | `Float` | `f` |
+| `Float64` | 64-bit float type | `Float` | `d` |
+| `Double` | An alias for `Float64` | `Float64` | `d` |
+| `Pointer` | A signed integer type of system size | `Size` | `P` |
+| `Bytes` | Unbounded bytes type | `bytes` | `p` |
+| `Bytes8` | 8-bit bytes type | `Bytes` | |
+| `Bytes16` | 16-bit bytes type | `Bytes` | |
+| `Bytes32` | 32-bit bytes type | `Bytes` | |
+| `Bytes64` | 64-bit bytes type | `Bytes` | |
+| `Bytes128` | 128-bit bytes type | `Bytes` | |
+| `Bytes256` | 256-bit bytes type | `Bytes` | |
+| `String` | Unbounded string type | `str` | `s` |
+| `Unicode` | Unbounded UTF-8 string type | `String` | |
+| `UTF8` | Unbounded UTF-8 string type | `Unicode` | |
+| `UTF16` | Unbounded UTF-16 string type | `Unicode` | |
+| `UTF32` | Unbounded UTF-32 string type | `Unicode` | |
+| `ASCII` | Unbounded ASCII string type | `String` | |
The unbounded types have no length/size restrictions on the values that they can hold
-beyond those imposed by the Python interpreter in use. The bounded types do impose a
+beyond those imposed by the Python interpreter being used. The bounded types do impose a
limit on the length/size of the values that they can hold, for example the `UInt8` type
can hold a minimum value of `0` and a maximum value of `255` being an 8-bit unsigned int
-value.
+value; larger values will overflow, be trimmed, or result in an exception being raised
+depending on the data type being used.
+
+While on some platforms, the `short`, `long`, and `long long` types and their `unsigned`
+equivalents guarantee a minimum of `16`, `32` or `64` bits respectively they are aliases
+for the equivalent fixed length types in this library. See the [**Data Type Sizes**](#data-type-sizes)
+section for more information on the types, and where applicable, their minimum and maximum values.
As each of the type classes ultimately subclass from one of the native Python data types
the class instances can be used interchangeably with their native Python counterparts.
@@ -121,23 +153,228 @@ 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.
+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
+the supported types. The class also supports indexed item and slice access semantics.
+
+The `ByteView` class offers the following methods:
+
+ * `__init__(data: bytes | bytearray)` – The `__init__()` method expects a `bytes` or `bytearray` object as input via the `data` argument, and will also accept any of the following optional keyword arguments:
+ * `split` (`int`) – The `split` argument defines how many bytes are put into each group for iterating over; by default it is set to `1` so each individual byte can be accessed independently; if the supplied `data` holds bytes that represent data that spans multiple bytes, say each two bytes represent a value, the `split` can be set to two, and groups of two bytes will be returned during iteration. The `split` length can also be changed at any time after initialisation, which can be especially useful when `data` holds bytes for mixed data types where it may be necessary to obtain two bytes, then four bytes, then two bytes, etc.
+
+ * `partial` (`bool`) – The `partial` argument defines if the view should allow iteration to continue after all whole groups have been iterated over; that is if the length of `data` is not evenly divisible by `split`, there could be a partial group of bytes left over at the end; say `data` is 10 bytes long, and `split` is set to `3`, there would be `3` groups of `3` bytes each, with `1` byte remaining; by default `partial` is set to `False` so this last group would not be returned during iteration; setting `partial` to `True` ensures that it is.
+
+ * `order` (`ByteOrder`) – The `order` argument defines the byte order of the data for decoding purposes; by default it is set to big-endian (`ByteOrder.MSB`), but can be set to little-endian via `ByteOrder.LSB` or one of the equivalent `ByteOrder` enumeration options, or one of the byte order characters supported by the `struct` module, `@`, `=`, `>`, `<` and `!`, can be used instead. See the [**Byte Order**](#byte-order) section for more information.
+
+ * `__len__()` (`int`) – The `__len__()` method returns the current number of items that the `BytesView` class can iterate over; the returned length is dependent upon the length of the assigned `data` value and the current `split` length, so the reported length can change if the `split` length is changed; it is also dependent upon if `partial` iteration is enabled or not, and if the length of `data` is evenly divisible by the `split` length or not.
+
+ * `__iter__()` (`BytesView`) – The `__iter__()` method supports iterating over a `BytesView` class instance using standard constructs such as a `for ... in ...` loop; an iterator can also be obtained by passing the class instance to the `iter()` standard library method.
+
+ * `__next__()` (`bytes` | `object`) – The `__next__()` method supports iterating over a `BytesView` class instance using standard constructs such as a `for ... in ...` loop, and returns the next available group of bytes in the view.
+
+ * `__getitem__(index: int)` (`bytes` | `object`) – The `__getitem__()` method supports item access to the groups of bytes in the view as defined by the group's index. If the specified index is out-of-bounds a `KeyError` exception will be raised.
+
+ * `split(length: int)` (`BytesView`) – The `split()` method supports changing the split length after class initialisation; it expects a positive integer value between `1` and the length in bytes of provided `data`, and returns a reference to `self` so calls to `split()` can be chained with further calls including iteration.
+
+ * `cast(type: Type | str, order: ByteOrder = None)` (`bytes` | `object`) – The `cast()` method supports casting the values held in the assigned `data` to one of the supported types offered by the `deliciousbytes` library, all of which are subclasses of native Python data types, so maybe used interchangeably. Using `cast()` implies a specific `split` length as each data type requires a certain number of raw bytes to be decoded into the native form. The `cast()` method returns a reference to `self` so calls to `cast()` can be chained with further calls including iteration.
+
+ * `next(type: Type | str = None, order: ByteOrder = None)` (`bytes` | `object`) – The `next()` method supports obtaining the next group of bytes in the view, or optionally casting the value to one of the supported types offered by the `deliciousbytes` library, all of which are subclasses of native Python data types, so maybe used interchangeably. Using `next()` implies a specific `split` length as each data type requires a certain number of raw bytes to be decoded into the native form, so when calling `next()` and specifying an optional `type`, the split length will be changed accordingly. The `next()` method may be called as many times as needed to obtain each group of bytes in the view, each time either with no defined `type` or with a different `type` if the data being decoded requires it.
+
+ * `tell()` (`int`) – The `tell()` method returns the current index position which is updated after each iteration; the `index` starts at `0` and is advanced during each iteration step, so at any given time it reports the index of the next item to be retrieved from the view.
+
+ * `seek(index: int)` (`BytesView`) – The `seek()` method provides support for moving the index to the specified position. The `seek()` method returns a reference to `self` so calls to `seek()` can be chained with further calls including iteration.
+
+ * `decode(format: str, order: ByteOrder = None, index: int = 0)` (`tuple[Type]`) – The `decode()` method provides support for decoding and casting a group of items to one or more of the `deliciousbytes.Type` subclasses by specifying a format string; the count of items returned depends upon the number of characters in the format string, less the optional byte order mark `>` or `<` at the beginning of the format string; note that the `.decode()` method always returns a tuple even if only a single value is decoded. Format strings do not need to provide instructions for decoding every value in the data, but if the format string requests more values than held in the provided raw data an exception will be raised.
+
+ * `encode(values: list[Type], order: ByteOrder = None)` (`BytesView`) – The `encode()` class method provides support for encoding one or more `deliciousbytes.Type` subclass instances to their underlying `bytes` values and concatenating those `bytes` to form the input data for a `BytesView` class instance that can then be used to further work with and manipulate the data as needed.
+
+The `ByteView` class offers the following properties:
+
+ * `data` (`bytes` | `bytearray`) – The `data` property returns the data held by the `BytesView` class.
+
+ * `splits` (`int`) – The `splits` property returns the current split length value; it can also be used to update the split length value in addition to calling the `split()` method; if setting `splits` it must be assigned to a positive `int` value between `1` and the length in bytes of the `data` assigned to the class.
+
+ * `partial` (`bool`) – The `partial` property returns the current partial iteration status, where `True` indicates that iteration will include any partial groups of bytes at the end of the list, as detailed above, and `False` indicates that iteration will stop after iterating over all whole groups of bytes. The `partial` property can also be used to update the `partial` property; if setting `partial` it must be assigned to a `bool` value.
+
+ * `order` (`ByteOrder`) – The `order` property returns the current byte order configured for the view; it can also be used to update the byte order; if setting `order` it must be assigned to a `ByteOrder` enumeration option.
+
+```python
+from deliciousbytes import (
+ Int, Int8, ByteOrder, BytesView,
+)
+
+data: bytes = b"\x00\x01\x00\x02\x00\x03\x00\x04"
+
+view = BytesView(data, split=2)
+
+assert isinstance(view, BytesView)
+
+# The length reflects the current length of the data as divided by the split size
+# This is the number of items that can iterated over in the view where the maximum index
+# that can be used during iteration or item level access is the reported length - 1.
+assert len(view) == 4
+
+# The items can be iterated over using normal iterator semantics such as for/enumerate
+for index, val in enumerate(view):
+ if index == 0:
+ assert val == b"\x00\x01"
+ elif index == 1:
+ assert val == b"\x00\x02"
+ elif index == 2:
+ assert val == b"\x00\x03"
+ elif index == 3:
+ assert val == b"\x00\x04"
+
+# Individual groups of bytes (based on the split size) can be accessed using item access
+assert view[0] == b"\x00\x01"
+assert view[1] == b"\x00\x02"
+assert view[2] == b"\x00\x03"
+assert view[3] == b"\x00\x04"
+
+# Note: When slicing access is used, the current split size is ignored
+
+# Test obtaining bytes from 1 until 4 (i.e. bytes 1, 2, 3)
+assert view[1:4] == b"\x01\x00\x02"
+
+# Test obtaining bytes from 0 until 4 (i.e. bytes 0, 1, 2, 3)
+assert view[0:4:+1] == b"\x00\x01\x00\x02"
+
+# Test obtaining bytes from 0 until 8, stepping 2 bytes each time
+assert view[0:8:+2] == b"\x00\x00\x00\x00"
+
+# Test obtaining bytes from 1 until 8, stepping 2 bytes each time
+assert view[1:8:+2] == b"\x01\x02\x03\x04"
+
+# Test obtaining bytes from 0 until 8, stepping -2 bytes each time, i.e. reversed
+assert view[1:8:-2] == b"\x04\x03\x02\x01"
+
+# Test obtaining bytes from 0 until 4, stepping -1 bytes each time, i.e. reversed
+assert view[0:4:-1] == b"\x02\x00\x01\x00"
+
+# The split length can be changed at any point
+for index, val in enumerate(view.split(4)):
+ if index == 0:
+ assert val == b"\x00\x01\x00\x02"
+ elif index == 1:
+ assert val == b"\x00\x03\x00\x04"
+
+# The last split length will be remembered (!)
+assert view[0] == b"\x00\x01\x00\x02"
+assert view[1] == b"\x00\x03\x00\x04"
+
+# Item values can be cast from raw bytes to the defined type; note that casting implies
+# an associated split size as each type cast requires the relevant number of bytes for
+# decoding into the defined type:
+for index, val in enumerate(view.cast(">h")):
+ if index == 0:
+ assert val == 1
+ elif index == 1:
+ assert val == 2
+ elif index == 2:
+ assert val == 3
+ elif index == 3:
+ assert val == 4
+
+# Items can be cast as a group via .decode() by specifying a format string; the count of
+# items returned depends upon the number of characters in the format string, less the
+# optional byte order mark `>` or `<` at the beginning of the format string; note that
+# the .decode() method always returns a tuple even if only a single value is decoded:
+assert view.decode(">h") == (1, )
+assert view.decode(">hh") == (1, 2)
+assert view.decode(">hhh") == (1, 2, 3)
+assert view.decode(">hhhh") == (1, 2, 3, 4)
+```
+
+
### Byte Order
The byte order for each of the data type classes defaults to most-significant bit first,
-MSB, but may be changed to 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 depending on how one prefers to
-refer to endianness:
-
-| Enumeration Option | Byte Order |
-|--------------------------------|------------|
-| `ByteOrder.MSB` | MSB |
-| `ByteOrder.LSB` | LSB |
-| `ByteOrder.Motorolla` | MSB |
-| `ByteOrder.Intel` | LSB |
-| `ByteOrder.BigEndian` | MSB |
-| `ByteOrder.LittleEndian` | LSB |
-
+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
+depending on how one prefers to refer to endianness:
+
+| Enumeration Option | Byte Order | Endianness |
+|--------------------------------|------------|------------|
+| `ByteOrder.MSB` | MSB | Big |
+| `ByteOrder.LSB` | LSB | Little |
+| `ByteOrder.Motorolla` | MSB | Big |
+| `ByteOrder.Intel` | LSB | Little |
+| `ByteOrder.BigEndian` | MSB | Big |
+| `ByteOrder.LittleEndian` | LSB | Little |
+| `ByteOrder.Big` | MSB | Big |
+| `ByteOrder.Little` | LSB | Little |
+| `ByteOrder.Native` | Native | Native |
+
+* The `ByteOrder.Native` option will report the system's native byte, returning either
+`ByteOrder.MSB` for big endian systems or `ByteOrder.LSB` for little endian systems.
+
+The `struct` module byte order mark equivalents are handled as follows:
+
+| Struct Byte Order Mark | Byte Order | Alignment | Notes |
+|------------------------|------------|-----------|------------------------------------|
+| `@` | System | Native | Alignment is not handled |
+| `=` | System | N/A | |
+| `>` | MSB | N/A | |
+| `<` | LSB | N/A | |
+| `!` | MSB | N/A | Most network protocols use MSB |
+
+If either system dependent byte order mark, `@` or `=` is specified, the byte order will
+be determined based on the endianness reported by Python's `sys.byteorder` property, so
+systems reporting `big` endianness will map to `ByteOrder.MSB` and systems reporting
+`little` endianness will map to `ByteOrder.LSB`.
+
+
+### Data Type Sizes
+
+| Class | Bytes | Minimum Value | Maximum Value |
+|--------------------|:-----:|----------------------------|----------------------------|
+| `Int` | `1+` | (depends on system) | (depends on system) |
+| `Int8` | `1` | -127 | +128 |
+| `Int16` | `2` | -32,768 | +32,767 |
+| `Int32` | `4` | -2,147,483,648 | +2,147,483,647 |
+| `Int64` | `8` | -9,223,372,036,854,775,808 | +9,223,372,036,854,775,807 |
+| `UInt` | `1+` | +0 | (depends on system) |
+| `UInt8` | `1` | +0 | +255 |
+| `UInt16` | `2` | +0 | +65,535 |
+| `UInt32` | `4` | +0 | +429,496,729 |
+| `UInt64` | `8` | +0 | +1.844,674,407,370,955e19 |
+| `Char` | `1` | +0 | +255 |
+| `SignedChar` | `1` | -127 | +128 |
+| `UnsignedChar` | `1` | +0 | +255 |
+| `Short` | `2` | -32,768 | +32,767 |
+| `SignedShort` | `2` | -32,768 | +32,767 |
+| `UnsignedShort` | `2` | +0 | +65,535 |
+| `Long` | `4` | -2,147,483,648 | +2,147,483,647 |
+| `SignedLong` | `4` | -2,147,483,648 | +2,147,483,647 |
+| `UnsignedLong` | `4` | +0 | +429,496,729 |
+| `LongLong` | `8` | -9,223,372,036,854,775,808 | +9,223,372,036,854,775,807 |
+| `SignedLongLong` | `8` | -9,223,372,036,854,775,808 | +9,223,372,036,854,775,807 |
+| `UnsignedLongLong` | `8` | +0 | +1.844,674,407,370,955e19 |
+| `Size` | `1+` | +0 | (depends on system) |
+| `SignedSize` | `1+` | (depends on system) | (depends on system) |
+| `UnsignedSize` | `1+` | +0 | (depends on system) |
+| `Float` | `8` | ≈ -1.7976931348623157e+308 | ≈ 1.7976931348623157e+308 |
+| `Float16` | `2` | -65,504 | +65,504 |
+| `Float32` | `4` | ≈ -1.17549435e-38 | ≈ 3.4028235e38 |
+| `Float64` | `8` | ≈ -1.7976931348623157e+308 | ≈ 1.7976931348623157e+308 |
+| `Double` | `8` | ≈ -1.7976931348623157e+308 | ≈ 1.7976931348623157e+308 |
+| `Bytes` | `1+` | (storage for 1 byte) | (depends on system) |
+| `Bytes8` | `1` | (storage for 1 byte) | (storage for 1 byte) |
+| `Bytes16` | `2` | (storage for 2 bytes) | (storage for 2 bytes) |
+| `Bytes32` | `4` | (storage for 4 bytes) | (storage for 4 bytes) |
+| `Bytes64` | `8` | (storage for 8 bytes) | (storage for 8 bytes) |
+| `Bytes128` | `16` | (storage for 16 bytes) | (storage for 16 bytes) |
+| `Bytes256` | `32` | (storage for 32 bytes) | (storage for 32 bytes) |
+| `String` | `1+` | (storage for 1 character) | (depends on system) |
+| `Unicode` | `1+` | (storage for 1 character) | (depends on system) |
+| `UTF8` | `1+` | (storage for 1 character) | (depends on system) |
+| `UTF16` | `1+` | (storage for 1 character) | (depends on system) |
+| `UTF32` | `1+` | (storage for 1 character) | (depends on system) |
+| `ASCII` | `1+` | (storage for 1 character) | (depends on system) |
+
+
### Unit Tests
The DeliciousBytes library includes a suite of comprehensive unit tests which ensure that
diff --git a/requirements.txt b/requirements.txt
index 3b84845..d3bbb73 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
# DeliciousBytes Library: Runtime Dependencies
-enumerific>=1.0.1
\ No newline at end of file
+enumerific>=1.0.1
+classicist>=1.0.0
\ No newline at end of file
diff --git a/source/deliciousbytes/__init__.py b/source/deliciousbytes/__init__.py
index a811262..e3bb552 100644
--- a/source/deliciousbytes/__init__.py
+++ b/source/deliciousbytes/__init__.py
@@ -3,6 +3,13 @@
import ctypes
import logging
import enumerific
+import struct
+import typing
+import sys
+import math
+import builtins
+
+from classicist import classproperty
logger = logging.getLogger(__name__)
@@ -21,7 +28,7 @@ class Encoding(enumerific.Enumeration, aliased=True):
class ByteOrder(enumerific.Enumeration, aliased=True):
- """Define the two styles of byte ordering - big-endian and little-endian"""
+ """Define the two styles of byte ordering - big-endian and little-endian."""
# Most significant byte ordering
MSB = "big"
@@ -37,12 +44,66 @@ class ByteOrder(enumerific.Enumeration, aliased=True):
BigEndian = MSB
LittleEndian = LSB
+ # Endian aliases (shorter)
+ Big = MSB
+ Little = LSB
+
+ @classproperty
+ def Native(cls) -> ByteOrder:
+ if sys.byteorder == "big":
+ return ByteOrder.MSB
+ elif sys.byteorder == "little":
+ return ByteOrder.LSB
+
+
+class Type(object):
+ """The Type class is the superclass for all deliciousbytes types and defines shared
+ properties and behaviour for each type class."""
+
+ _length: int = None
+ _signed: bool = None
+ _format: str = None
+ _order: ByteOrder = None
+
+ @classproperty
+ def length(cls) -> int | None:
+ """Return the number of bytes that are used to hold the value."""
+ return cls._length
+
+ @classproperty
+ def signed(cls) -> bool | None:
+ """Return whether the type is signed or not."""
+ return cls._signed
+
+ @classproperty
+ def format(cls) -> str | None:
+ """Return the format character used for the type in the struct module if set."""
+ return cls._format
+
+ @property
+ def order(self) -> ByteOrder:
+ """Return the current byte order associated with the type."""
+
+ return self._order
+
+ @order.setter
+ def order(self, order: ByteOrder):
+ """Support changing the byte order used to encode the type."""
+
+ if not isinstance(order, ByteOrder):
+ raise TypeError(
+ "The 'order' argument must have a ByteOrder enumeration value!"
+ )
+
+ self._order = order
+
-class Int(int):
- """Signed integer type, defaults to 64-bits, 8 bytes of width."""
+class Int(int, Type):
+ """Signed unbounded integer type."""
- _length: int = 8 # 8 bytes, 64-bit signed integer
+ _length: int = 0 # Unbounded length integer limited only by available system memory
_signed: bool = True
+ _format: str = None
_order: ByteOrder = ByteOrder.MSB
# In Python 3, the int type is unbounded and can store arbitrarily large numbers and
@@ -79,6 +140,15 @@ def __bytes__(self) -> bytes:
def __len__(self) -> int:
return len(bytes(self))
+ def __int__(self) -> int:
+ return int.__int__(self)
+
+ def __float__(self) -> float:
+ return float.__float__(self)
+
+ def __bool__(self) -> bool:
+ return self > 0
+
def __getitem__(self, key: int) -> bytes:
"""Support obtaining individual bytes from the encoded version of the value."""
@@ -256,41 +326,12 @@ def __invert__(self) -> Int:
"""Unary invert"""
return self.__class__(~int(self))
- @property
- def length(self) -> int:
- """Return the number of bytes that are used to hold the value."""
- return self._length
-
- @property
- def signed(self) -> bool:
- """Return whether the type is signed or not."""
- return self._signed
-
- @property
- def order(self) -> ByteOrder:
- """Return the current byte order associated with the type."""
-
- return self._order
-
- @order.setter
- def order(self, order: ByteOrder):
- """Support changing the byte order used to encode the type."""
-
- if not isinstance(order, ByteOrder):
- raise TypeError(
- "The 'order' argument must have a ByteOrder enumeration value!"
- )
-
- self._order = order
-
- @classmethod
- @property
+ @classproperty
def MIN(cls) -> int:
"""Return the minimum value that can be held by the type."""
return cls._minimum
- @classmethod
- @property
+ @classproperty
def MAX(cls) -> int:
"""Return the maximum value that can be held by the type."""
return cls._maximum
@@ -303,21 +344,32 @@ def encode(self, order: ByteOrder = None) -> bytes:
"The 'order' argument must have a ByteOrder enumeration value!"
)
- return self.to_bytes(
- length=self.length, byteorder=order.value, signed=self.signed
- )
+ if self.length > 0:
+ return self.to_bytes(
+ length=self.length, byteorder=order.value, signed=self.signed
+ )
+ else:
+ return self.to_bytes(
+ length=math.ceil(self.bit_length() / 8),
+ byteorder=order.value,
+ signed=self.signed,
+ )
@classmethod
- def decode(cls, value: bytes, order: ByteOrder = ByteOrder.MSB) -> Int:
- if not isinstance(value, bytes):
- raise TypeError("The 'value' argument must have a bytes value!")
+ def decode(cls, value: bytes | bytearray, order: ByteOrder = ByteOrder.MSB) -> Int:
+ if not isinstance(value, (bytes, bytearray)):
+ raise TypeError(
+ "The 'value' argument must have a bytes or bytearray value!"
+ )
if not isinstance(order, ByteOrder):
raise TypeError(
"The 'order' argument must have a ByteOrder enumeration value!"
)
- decoded = cls(int.from_bytes(value, byteorder=order.value, signed=cls._signed))
+ decoded = cls(
+ int.from_bytes(bytes(value), byteorder=order.value, signed=cls._signed)
+ )
logger.debug(
"%s.decode(value: %r, order: %r) => %r", cls.__name__, value, order, decoded
@@ -327,6 +379,8 @@ def decode(cls, value: bytes, order: ByteOrder = ByteOrder.MSB) -> Int:
class Int8(Int):
+ """An signed 1-byte, 8-bit integer type."""
+
_length: int = 1
_signed: bool = True
_minimum: int = -128
@@ -342,6 +396,8 @@ def __new__(cls, value: int, *args, **kwargs):
class Int16(Int):
+ """An signed 2-byte, 16-bit integer type."""
+
_length: int = 2
_signed: bool = True
_minimum: int = -32768
@@ -357,6 +413,8 @@ def __new__(cls, value: int, *args, **kwargs):
class Int32(Int):
+ """An signed 4-byte, 32-bit integer type."""
+
_length: int = 4
_signed: bool = True
_minimum: int = -2_147_483_648
@@ -372,6 +430,8 @@ def __new__(cls, value: int, *args, **kwargs):
class Int64(Int):
+ """An signed 8-byte, 64-bit integer type."""
+
_length: int = 8
_signed: bool = True
_minimum: int = -9_223_372_036_854_775_808
@@ -387,6 +447,8 @@ def __new__(cls, value: int, *args, **kwargs):
class UInt(Int):
+ """An unsigned unbounded integer type."""
+
_length: int = None
_signed: bool = False
_minimum: int = 0
@@ -394,6 +456,8 @@ class UInt(Int):
class UInt8(UInt):
+ """An unsigned 1-byte, 8-bit wide integer type."""
+
_length: int = 1
_minimum: int = 0
_maximum: int = 255
@@ -408,6 +472,8 @@ def __new__(cls, value: int, *args, **kwargs):
class UInt16(UInt):
+ """An unsigned 2-byte, 16-bit wide integer type."""
+
_length: int = 2
_minimum: int = 0
_maximum: int = 65535
@@ -422,6 +488,8 @@ def __new__(cls, value: int, *args, **kwargs):
class UInt32(UInt):
+ """An unsigned 4-byte, 32-bit wide integer type."""
+
_length: int = 4
_minimum: int = 0
_maximum: int = 4294967295
@@ -436,6 +504,8 @@ def __new__(cls, value: int, *args, **kwargs):
class UInt64(UInt):
+ """An unsigned 8-byte, 64-bit wide integer type."""
+
_length: int = 8
_minimum: int = 0
_maximum: int = 1.844674407370955e19
@@ -450,6 +520,11 @@ def __new__(cls, value: int, *args, **kwargs):
class Char(UInt8):
+ """A char is an unsigned 1-byte, 8-bits wide integer type."""
+
+ _format: str = "c"
+ _signed: bool = False
+
def __new__(cls, value: int | str | bytes, *args, **kwargs):
if not isinstance(value, (int, str, bytes)):
raise ValueError(
@@ -474,6 +549,11 @@ def __str__(self) -> str:
class SignedChar(Int8):
+ """A signed char is an signed 1-byte, 8-bits wide integer type."""
+
+ _format: str = "b"
+ _signed: bool = True
+
def __new__(cls, value: int | str | bytes, *args, **kwargs):
if not isinstance(value, (int, str, bytes)):
raise ValueError(
@@ -497,43 +577,384 @@ def __str__(self) -> str:
return chr(self)
-class Short(UInt16):
- """A short integer is an unsigned integer at least 16-bits wide."""
-
+class UnsignedChar(Char):
pass
+class Short(Int16):
+ """A short integer is an signed 2-byte, 16-bits wide integer type."""
+
+ _format: str = "h"
+
+
class SignedShort(Int16):
- """A signed short integer is an signed integer at least 16-bits wide."""
+ """A short integer is an signed 2-byte, 16-bits wide integer type."""
- pass
+ _format: str = "h"
-class Long(UInt32):
- """A long integer is an unsigned integer at least 32-bits wide."""
+class UnsignedShort(UInt16):
+ """An unsigned short integer is an unsigned 2-byte, 16-bits wide integer type."""
- pass
+ _format: str = "H"
+
+
+class Long(Int32):
+ """A long integer is an signed 4-byte, 32-bits wide integer type."""
+
+ _format: str = "l"
class SignedLong(Int32):
- """A signed long integer is an signed integer at least 32-bits wide."""
+ """A long integer is an signed 4-byte, 32-bits wide integer type."""
- pass
+ _format: str = "l"
-class LongLong(UInt64):
- """A long long integer is an unsigned integer at least 64-bits wide."""
+class UnsignedLong(UInt32):
+ """An unsigned long integer is an unsigned 4-byte, 32-bits wide integer type."""
- pass
+ _format: str = "L"
+
+
+class LongLong(Int64):
+ """A long long integer is an signed 8-byte, 64-bits wide integer type."""
+
+ _format: str = "q"
+
+
+# An alias for LongLong
+class SignedLongLong(Int64):
+ """A long long integer is an signed 8-byte, 64-bits wide integer type."""
+
+ _format: str = "q"
+
+
+class UnsignedLongLong(UInt64):
+ """An unsinged long long integer is an unsigned 8-byte, 64-bits wide integer type."""
+
+ _format: str = "Q"
+
+
+class Size(UInt):
+ """An unsigned integer type of the maximum byte width supported by the system."""
+
+ # Determine the maximum system size for an integer
+ for size, x in enumerate(range(0, 8), start=1):
+ if sys.maxsize == (pow(2, pow(2, x) - 1) - 1):
+ break
+ else:
+ size = 0
+
+ # sys.maxsize accounts for signing, so returns a 1 byte less to account for this
+ _length: int = (size + 1) if size > 0 else 0
+ _signed: bool = False
+
+
+class SignedSize(Size):
+ """An signed integer type of the maximum byte width supported by the system."""
+
+ _signed: bool = True
+
+
+class UnsignedSize(Size):
+ """An unsigned integer type of the maximum byte width supported by the system."""
+
+ _signed: bool = False
+
+
+class Float(float, Type):
+ """Signed double-precision float type, 64-bits, 8-bytes of width."""
+
+ _length: int = 8 # 8-byte, 64-bit signed float
+ _format: str = "d" # double-precision float
+ _signed: bool = True
+ _order: ByteOrder = ByteOrder.MSB
+ _minimum: float = float("-inf")
+ _maximum: float = float("inf")
+
+ def __new__(cls, value: float, **kwargs):
+ logger.debug(
+ "%s.__new__(cls: %s, value: %s, kwargs: %s)",
+ cls.__name__,
+ cls,
+ value,
+ kwargs,
+ )
+
+ if not isinstance(value, (int, float)):
+ raise ValueError(
+ "The 'value' argument must have an integer or float value!"
+ )
+
+ return super().__new__(cls, value)
+
+ def __bytes__(self) -> bytes:
+ return self.encode()
+
+ def __float__(self) -> float:
+ return self
+
+ def __int__(self) -> int:
+ return int(self)
+
+ def __bool__(self) -> bool:
+ return self > 0.0
+
+ def __len__(self) -> int:
+ return len(bytes(self))
+
+ def __getitem__(self, key: int) -> bytes:
+ """Support obtaining individual bytes from the encoded version of the value."""
+
+ encoded: bytes = bytes(self)
+
+ if not (isinstance(key, int) and key >= 0):
+ raise TypeError("The 'key' argument must have a positive integer value!")
+
+ if key >= len(encoded):
+ raise KeyError(
+ "The 'key' argument must have a positive integer value that is in range of the element indicies that are available!"
+ )
+
+ return encoded[key]
+
+ def __setitem__(self, key: int, value: int):
+ raise NotImplementedError
+
+ def __delitem__(self, key: int, value: int):
+ raise NotImplementedError
+
+ def __add__(self, other: float | int) -> Float:
+ """Addition"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) + float(other))
+
+ def __mul__(self, other: float | int) -> Float:
+ """Multiply"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) * float(other))
+
+ def __truediv__(self, other: float) -> Float:
+ """True division"""
+ if not isinstance(other, (int, float)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) / float(other))
+
+ def __floordiv__(self, other: float | int) -> Float:
+ """Floor division"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) // float(other))
+
+ def __sub__(self, other: float | int) -> Float:
+ """Subtraction"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) - float(other))
+
+ def __mod__(self, other: float | int) -> Float:
+ """Modulo"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) % float(other))
+
+ def __pow__(self, other: float | int) -> Float:
+ """Power"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) ** float(other))
+
+ def __rshift__(self, other: float | int) -> Float:
+ """Right bit shift"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) >> float(other))
+
+ def __lshift__(self, other: float | int) -> Float:
+ """Left bit shift"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) << float(other))
+
+ def __and__(self, other: float | int) -> Float:
+ """Binary AND"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) & float(other))
+
+ def __or__(self, other: float | int) -> Float:
+ """Binary OR"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) | float(other))
+
+ def __xor__(self, other: float | int) -> Float:
+ """Binary XOR"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) ^ float(other))
+
+ def __iadd__(self, other: float | int) -> Float:
+ """Asignment addition"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) + float(other))
+
+ def __imul__(self, other: float | int) -> Float:
+ """Asignment multiply"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) * float(other))
+
+ def __idiv__(self, other: float | int) -> Float:
+ """Asignment true division"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) / float(other))
+
+ def __ifloordiv__(self, other: float | int) -> Float:
+ """Asignment floor division"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) // float(other))
+ def __isub__(self, other: float | int) -> Float:
+ """Asignment subtract"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) - float(other))
+
+ def __imod__(self, other: float | int) -> Float:
+ """Asignment modulo"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) % float(other))
+
+ def __ipow__(self, other: float | int) -> Float:
+ """Asignment power"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) ** float(other))
+
+ def __irshift__(self, other: float | int) -> Float:
+ """Asignment right bit shift"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) >> float(other))
-class SignedLongLong(UInt64):
- """A signed long long integer is an signed integer at least 64-bits wide."""
+ def __ilshift__(self, other: float | int) -> Float:
+ """Asignment left bit shift"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) << float(other))
+
+ def __iand__(self, other: float | int) -> Float:
+ """Asignment AND"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) & float(other))
+
+ def __ior__(self, other: float | int) -> Float:
+ """Asignment OR"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) | float(other))
+
+ def __ixor__(self, other: float | int) -> Float:
+ """Asignment XOR"""
+ if not isinstance(other, (float, int)):
+ raise TypeError("The 'other' argument must have a float or integer value!")
+ return self.__class__(float(self) ^ float(other))
+
+ def __neg__(self) -> Float:
+ """Unary negation"""
+ return self.__class__(-float(self))
+
+ def __pos__(self) -> Float:
+ """Unary positive"""
+ return self.__class__(+float(self))
+
+ def __invert__(self) -> Float:
+ """Unary invert"""
+ return self.__class__(~float(self))
+ @classproperty
+ def MIN(cls) -> float:
+ """Return the minimum value that can be held by the type."""
+ return cls._minimum
+
+ @classproperty
+ def MAX(cls) -> float:
+ """Return the maximum value that can be held by the type."""
+ return cls._maximum
+
+ def encode(self, order: ByteOrder = ByteOrder.MSB) -> bytes:
+ if not isinstance(order, ByteOrder):
+ raise TypeError(
+ "The 'order' argument must reference a ByteOrder enumeration option!"
+ )
+
+ if order is ByteOrder.MSB:
+ format = f">{self._format}"
+ elif order is ByteOrder.LSB:
+ format = f"<{self._format}"
+
+ return struct.pack(format, self)
+
+ @classmethod
+ def decode(
+ cls, value: bytes | bytearray, order: ByteOrder = ByteOrder.MSB
+ ) -> Float:
+ if not isinstance(value, (bytes, bytearray)):
+ raise TypeError(
+ "The 'value' argument must have a bytes or bytearray value!"
+ )
+ elif not len(value) == cls._length:
+ raise TypeError(
+ f"The 'value' argument must have a length of {cls._length} bytes!"
+ )
+
+ if not isinstance(order, ByteOrder):
+ raise TypeError(
+ "The 'order' argument must reference a ByteOrder enumeration option!"
+ )
+
+ if order is ByteOrder.MSB:
+ format = f">{cls._format}"
+ elif order is ByteOrder.LSB:
+ format = f"<{cls._format}"
+
+ return cls(value=struct.unpack(format, bytes(value))[0])
+
+
+class Float16(Float):
+ _length: int = 2 # 2-byte, 16-bit signed float
+ _format: str = "e" # single-precision 2-byte, 16-bit float
+
+
+class Float32(Float):
+ _length: int = 4 # 4-byte, 32-bit signed float
+ _format: str = "f" # single-precision 4-byte, 32-bit float
+
+
+class Float64(Float):
+ _length: int = 8 # 8-byte, 64-bit signed float
+ _format: str = "d" # double-precision float
+
+
+class Double(Float):
+ _length: int = 8 # 8-byte, 64-bit signed float
+ _format: str = "d" # double-precision float
+
+
+class Pointer(Size):
pass
-class Bytes(bytes):
+class Bytes(bytes, Type):
_length: int = None
def __new__(cls, value: bytes | bytearray | Int, length: int = None):
@@ -558,10 +979,6 @@ def __new__(cls, value: bytes | bytearray | Int, length: int = None):
return self
- @property
- def length(self) -> int | None:
- return self._length
-
def encode(
self, order: ByteOrder = ByteOrder.MSB, length: int = None, raises: bool = True
) -> bytes:
@@ -606,8 +1023,16 @@ def encode(
return bytes(encoded)
@classmethod
- def decode(value: bytes) -> Bytes:
- pass
+ def decode(cls, value: bytes | bytearray, order: ByteOrder = None) -> Bytes:
+ if not isinstance(value, (bytes, bytearray)):
+ raise TypeError(
+ "The 'value' argument must have a bytes or bytearray value!"
+ )
+
+ if order is ByteOrder.LSB:
+ value = reversed(value)
+
+ return cls(value=bytes(value))
class Bytes8(Bytes):
@@ -634,23 +1059,33 @@ class Bytes256(Bytes):
_length: int = 32 # 32 bytes = 256 bits (32 * 8 = 256)
-class String(str):
+class String(str, Type):
+ """An unbounded string type which defaults to Unicode (UTF-8) encoding."""
+
+ _encoding: Encoding = Encoding.UTF8 # Default encoding for Python 3 strings is UTF8
+
def __new__(cls, value: str, *args, **kwargs):
return super().__new__(cls, value, *args, **kwargs)
+ @classproperty
+ def encoding(cls) -> Encoding:
+ return cls._encoding
+
def encode(
self,
order: ByteOrder = ByteOrder.MSB,
- encoding: Encoding = Encoding.Unicode,
+ encoding: Encoding = None,
):
- if not isinstance(encoding, Encoding):
+ if encoding is None:
+ encoding = self.encoding
+ elif not isinstance(encoding, Encoding):
raise TypeError(
- "The 'encoding' argument must reference an Encoding enumeration option!"
+ "The 'encoding' argument, if specified, must reference an Encoding enumeration option!"
)
if order is ByteOrder.MSB:
return bytes(bytearray(str.encode(self, encoding.value)))
- elif order is ByteOrder.MSB:
+ elif order is ByteOrder.LSB:
return bytes(reversed(bytearray(str.encode(self, encoding.value))))
else:
raise TypeError(
@@ -662,7 +1097,7 @@ def decode(
cls,
value: bytes,
order: ByteOrder = ByteOrder.MSB,
- encoding: Encoding = Encoding.Unicode,
+ encoding: Encoding = None,
) -> String:
if not isinstance(value, bytes):
raise TypeError("The 'value' argument must have a 'bytes' value!")
@@ -672,12 +1107,537 @@ def decode(
"The 'order' argument must reference a ByteOrder enumeration option!"
)
- if not isinstance(encoding, Encoding):
+ if encoding is None:
+ encoding = cls.encoding
+ elif not isinstance(encoding, Encoding):
raise TypeError(
- "The 'encoding' argument must reference an Encoding enumeration option!"
+ "The 'encoding' argument, if specified, must reference an Encoding enumeration option!"
)
if order is ByteOrder.LSB:
value = bytes(reversed(bytearray(value)))
- return String(value.decode(encoding.value))
+ return cls(value.decode(encoding.value))
+
+
+class Unicode(String):
+ """An unbounded string type which defaults to Unicode (UTF-8) encoding."""
+
+ _encoding: Encoding = Encoding.UTF8
+
+
+class UTF8(Unicode):
+ """An unbounded string type which defaults to Unicode (UTF-8) encoding."""
+
+ _encoding: Encoding = Encoding.UTF8
+
+
+class UTF16(Unicode):
+ """An unbounded string type which defaults to Unicode (UTF-16) encoding."""
+
+ _encoding: Encoding = Encoding.UTF16
+
+
+class UTF32(Unicode):
+ """An unbounded string type which defaults to Unicode (UTF-32) encoding."""
+
+ _encoding: Encoding = Encoding.UTF32
+
+
+class ASCII(String):
+ """An unbounded string type which defaults to ASCII encoding."""
+
+ _encoding: Encoding = Encoding.ASCII
+
+
+class BytesView(object):
+ _type: str = None
+
+ _types: dict[str, dict] = {
+ "x": {"size": 0, "signed": None, "type": None},
+ "c": {"size": 1, "signed": None, "type": Char},
+ "b": {"size": 1, "signed": True, "type": SignedChar},
+ "B": {"size": 1, "signed": False, "type": UnsignedChar},
+ "?": {"size": 1, "signed": None, "type": bool},
+ "h": {"size": 2, "signed": True, "type": Short},
+ "H": {"size": 2, "signed": False, "type": UnsignedShort},
+ "i": {"size": 4, "signed": True, "type": Int32},
+ "I": {"size": 4, "signed": False, "type": UInt32},
+ "l": {"size": 4, "signed": True, "type": Long},
+ "L": {"size": 4, "signed": False, "type": UnsignedLong},
+ "q": {"size": 8, "signed": True, "type": LongLong},
+ "Q": {"size": 8, "signed": False, "type": UnsignedLongLong},
+ "n": {"size": 0, "signed": True, "type": SignedSize},
+ "N": {"size": 0, "signed": False, "type": Size},
+ "e": {"size": 2, "signed": True, "type": Float16},
+ "f": {"size": 4, "signed": True, "type": Float32},
+ "d": {"size": 8, "signed": True, "type": Double},
+ "s": {"size": 0, "signed": None, "type": String},
+ "p": {"size": 0, "signed": None, "type": Bytes},
+ "P": {"size": 0, "signed": None, "type": Pointer},
+ }
+
+ _orders: list[str] = [
+ "@", # native (native size, native alignment)
+ "=", # native (standard size, no specific alignment)
+ "<", # little-endian (standard size, no specific alignment)
+ ">", # big-endian (standard size, no specific alignment)
+ "!", # network (big-endian, standard size, no specific alignment)
+ ]
+
+ def __init__(
+ self,
+ data: bytes | bytearray,
+ split: int = 1,
+ partial: bool = False,
+ order: ByteOrder = ByteOrder.MSB,
+ ):
+ if not isinstance(data, (bytes, bytearray)):
+ raise TypeError("The 'data' argument must have a bytes or bytearray value!")
+
+ self._data = bytearray(data)
+
+ self._size = len(self._data)
+
+ if not isinstance(split, int):
+ raise TypeError("The 'split' argument must have a positive integer value!")
+ elif 1 <= split < self._size:
+ self._splits = split
+ else:
+ raise ValueError(
+ "The 'split' argument must have a positive integer value between 1 and the length of the provided data, currently, %d!"
+ % (self._size)
+ )
+
+ self.partial = partial
+
+ self.order = order
+
+ def __len__(self) -> int:
+ parts: float = self._size / self._splits
+
+ if self.partial is True:
+ return math.ceil(parts)
+ else:
+ return math.floor(parts)
+
+ def __iter__(self) -> bytesview:
+ self._index = 0
+ return self
+
+ def __next__(self) -> bytes | object:
+ if self._index + self._splits > self._size:
+ raise StopIteration
+
+ value: bytes = self.data[self._index : self._index + self._splits]
+
+ self._index += self._splits
+
+ if isinstance(typed := self.typed, type):
+ value = typed.decode(
+ value=value,
+ order=self.byteorder,
+ )
+
+ return value
+
+ def __getitem__(self, index: int | slice) -> bytearray | object:
+ # logger.debug("%s.__getitem__(index: %r)" % (self.__class__.__name__, index))
+
+ maxindex: int = math.floor(self._size / self._splits) - 1
+ reverse: bool = False
+
+ if isinstance(index, slice):
+ start: int = index.start or 0
+ stop: int = index.stop or self._size
+ step: int = (0 - index.step) if (index.step or 1) < 0 else (index.step or 1)
+ reverse: bool = (index.step or 1) < 0
+ elif isinstance(index, int):
+ if index >= 0:
+ start: int = self._splits * index
+ stop: int = start + self._splits
+ step: int = 1
+ else:
+ raise ValueError(
+ "The 'index' argument must have a positive integer >= 0!"
+ )
+
+ if index > maxindex:
+ if self.partial is True:
+ if (self._splits + (self._size - end)) < 0:
+ raise IndexError(
+ f"The index, {index}, is out of range; based on the length of the current data, {self._size}, and split length, {self._splits}, index must be between 0 – {maxindex}!"
+ )
+ else:
+ stop = self._size
+ elif self.partial is False:
+ raise IndexError(
+ f"The index, {index}, is out of range; based on the length of the current data, {self._size}, and split length, {self._splits}, index must be between 0 – {maxindex}!"
+ )
+ else:
+ raise TypeError("The 'index' argument must have an integer or slice value!")
+
+ # logger.debug(start, stop, step, "r" if reverse else "f")
+
+ value = self.data[start:stop:step]
+
+ if reverse is True:
+ value = bytearray(reversed(value))
+
+ if isinstance(typed := self.typed, type):
+ value = typed.decode(
+ value=value,
+ order=self.byteorder,
+ )
+
+ return value
+
+ @property
+ def data(self) -> bytearray:
+ return self._data
+
+ @property
+ def splits(self) -> int:
+ return self._splits
+
+ @splits.setter
+ def splits(self, splits: int):
+ if not isinstance(splits, int):
+ raise TypeError("The 'splits' argument must have a positive integer value!")
+ elif not 1 <= splits < self._size:
+ raise ValueError(
+ "The 'splits' argument must have a positive integer value between 1 and the length of the provided data, currently, %d!"
+ % (self._size)
+ )
+ self._splits = splits
+
+ @property
+ def partial(self) -> bool:
+ return self._partial
+
+ @partial.setter
+ def partial(self, partial: bool):
+ if not isinstance(partial, bool):
+ raise TypeError("The 'partial' argument must have a boolean value!")
+ self._partial = partial
+
+ @property
+ def order(self) -> str:
+ return self._order
+
+ @order.setter
+ def order(self, order: str | ByteOrder):
+ if order is None:
+ self._order = "@"
+ elif isinstance(order, ByteOrder):
+ if order is ByteOrder.MSB:
+ self._order = ">"
+ elif order is ByteOrder.LSB:
+ self._order = "<"
+ elif not isinstance(order, str):
+ raise TypeError("The 'order' argument must have a string value!")
+ elif order in self.orders:
+ self._order = order
+ else:
+ raise ValueError(
+ "The 'order' argument, if specified, must have one of the following values: %s!"
+ % (", ".join(self.orders))
+ )
+
+ @property
+ def orders(self) -> list[str]:
+ return list(self._orders)
+
+ @property
+ def byteorder(self) -> ByteOrder:
+ if self.order == "@" or self.order == "=":
+ if sys.byteorder == "big":
+ return ByteOrder.MSB
+ elif sys.byteorder == "little":
+ return ByteOrder.LSB
+ elif self.order == ">":
+ return ByteOrder.MSB
+ elif self.order == "<":
+ return ByteOrder.LSB
+ elif self.order == "!":
+ return ByteOrder.MSB
+
+ @property
+ def type(self) -> str | None:
+ return self._type
+
+ @type.setter
+ def type(self, type: str):
+ if type is None:
+ self._type = None
+ elif not isinstance(type, str):
+ raise TypeError("The 'type' argument must have a string value!")
+ elif type in self.types:
+ self._type = type
+ else:
+ raise ValueError(
+ "The 'type' argument, if specified, must have one of the following values: '%s', not '%s'!"
+ % (
+ "', '".join(self.types.keys()),
+ type,
+ )
+ )
+
+ @property
+ def types(self) -> dict[str, dict]:
+ return dict(self._types)
+
+ @property
+ def typed(self) -> type | None:
+ if self.type:
+ if isinstance(typed := self.types[self.type]["type"], type):
+ logger.debug(
+ "%s.typed() => type => %r => class => %r",
+ self.__class__.__name__,
+ self.type,
+ typed,
+ )
+ return typed
+
+ def split(self, split: int = None) -> BytesView:
+ """The `split()` method supports changing the split length; it expects a value
+ between `1` and the length in bytes of provided `data`, and returns a reference
+ to `self` so calls can be chained with further calls including iteration."""
+
+ if not isinstance(split, int):
+ raise TypeError("The 'split' argument must have an integer value!")
+
+ self.splits = split
+
+ return self
+
+ def cast(self, type: str | Type | None, order: ByteOrder = None) -> BytesView:
+ """The `cast()` method supports casting the values held in the assigned `data`
+ to one of the supported types offered by the `deliciousbytes` library, all of
+ which are subclasses of native Python data types, so maybe used interchangeably.
+ Using `cast()` implies a specific `split` length as each data type requires a
+ certain number of raw bytes to be decoded into the native form. The `cast()`
+ method returns a reference to `self` so calls can be chained with further calls
+ including iteration."""
+
+ if type is None:
+ self.type = None
+ elif isinstance(type, builtins.type) and issubclass(type, Type):
+ if isinstance(format := type.format, str):
+ self.type = format
+ else:
+ raise TypeError(
+ f"The 'type' argument referenced a Type subclass, '{type.__name__}', that cannot be cast!"
+ )
+ elif not isinstance(type, str):
+ raise TypeError(
+ "The 'type' argument must have a string value or reference a Type subclass!"
+ )
+ else:
+ if not 1 <= len(type) <= 2:
+ raise ValueError(
+ "The 'type' argument must have a length between 1 - 2 characters!"
+ )
+ elif len(type) == 2:
+ self.order = type[0]
+ self.type = type = type[1]
+ elif isinstance(order, ByteOrder):
+ if order is ByteOrder.MSB:
+ self.order = ">"
+ elif order is ByteOrder.LSB:
+ self.order = "<"
+
+ if type in self.types:
+ self.type = type
+ self.splits = struct.calcsize(f"{self.order}{self.type}")
+
+ logger.debug(
+ "%s.cast() cast setup for type '%s%s' and length %d",
+ self.__class__.__name__,
+ self.order,
+ self.type,
+ self.splits,
+ )
+ else:
+ raise ValueError(
+ "The 'type' argument, if specified, must have one of the following values: %s!"
+ % (", ".join(self.types.keys()))
+ )
+
+ return self
+
+ def tell(self) -> int:
+ """The 'tell' method provides support for reporting current the index position."""
+
+ return self._index
+
+ def seek(self, index: int) -> BytesView:
+ """The 'seek' method provides support for moving the index to the specified position."""
+
+ if not isinstance(index, int):
+ raise TypeError("The 'index' argument must have an integer value!")
+ elif 0 <= index < len(self):
+ if self._splits > 1:
+ self._index = self._splits * index
+ else:
+ self._index = index
+ else:
+ raise TypeError(
+ f"The 'index' argument must have an integer value between 0 - {len(self) - 1}!"
+ )
+
+ return self
+
+ def next(self, type: str | Type = None, order: ByteOrder = None) -> object | None:
+ """The `next()` method supports obtaining the next group of bytes in the view,
+ or optionally casting the value to one of the supported types offered by the
+ `deliciousbytes` library, all of which are subclasses of native Python data
+ types, so maybe used interchangeably.
+
+ Using `next()` implies a specific `split` length as each data type requires a
+ certain number of raw bytes to be decoded into its native form, so when calling
+ `next()` and specifying an optional `type`, the split length will be changed
+ accordingly for the current instance and will be used until it is updated again.
+
+ The `next()` method may be called as many times as needed to obtain each group
+ of bytes in the view, each time either with no defined type or with a different
+ type each time, if the data being decoded requires it."""
+
+ if type is None:
+ pass
+ elif not isinstance(type, (str, Type)):
+ raise TypeError(
+ "The 'type' argument, if specified, must have a string value or reference a Type subclass!"
+ )
+
+ try:
+ return next(self.cast(type=type, order=order))
+ except StopIteration:
+ return None
+
+ def decode(
+ self, format: str, order: ByteOrder = None, index: int = 0
+ ) -> tuple[Type]:
+ """The decode method supports decoding the data held by a BytesView into a tuple
+ of deliciousbytes.Type instances, which all subclass native Python types and may
+ be used interchangably with native types.
+
+ To decode raw bytes data held by a BytesView instance, specify a format string
+ of one character per type to be decoded from the underlaying data; each type is
+ specified by a character as per those defined in the `struct` module.
+
+ The format string does not need to be long enough to decode all of the data held
+ 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(format, str):
+ raise TypeError("The 'format' argument must have a string value!")
+ elif not len(format := format.strip()) > 0:
+ raise TypeError("The 'format' argument must have a non-empty string value!")
+
+ if len(format) >= 2 and format[0] in self.orders:
+ order = format[0]
+ format = format[1:]
+
+ if order is None:
+ byteorder: str = "@"
+ elif isinstance(order, str) and order in self.orders:
+ byteorder: str = order
+ elif isinstance(order, ByteOrder):
+ if order is ByteOrder.MSB:
+ byteorder: str = ">"
+ elif order is ByteOrder.LSB:
+ byteorder: str = "<"
+ else:
+ raise TypeError(
+ "The 'order' argument, if specified, must reference a ByteOrder enumeration option!"
+ )
+
+ if not isinstance(index, int):
+ raise TypeError("The 'index' argument must have an integer value!")
+ elif not 0 <= index < (datasize := len(self.data)):
+ raise TypeError(
+ f"The 'index' argument must have a positive integer value between 0 - {datasize}!"
+ )
+
+ if (calcsize := struct.calcsize(f"{byteorder}{format}")) > datasize:
+ raise ValueError(
+ f"The 'format' string specifies data types for more raw data than is available; the format string is calculated to require {calcsize} bytes, while the view currently holds {datasize} bytes!"
+ )
+
+ self.seek(index)
+
+ types: list[str] = []
+
+ number: str = ""
+
+ # Support struct-style format strings which allow spaces and repeated types
+ for char in format:
+ if char.isspace():
+ number = "" # reset number; spaces cannot be between numbers and types
+ continue # spaces are ignored
+ elif char in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]:
+ number += char
+ elif char in self.types:
+ if len(number) > 0:
+ if (count := int(number)) > 0:
+ for i in range(0, count):
+ types.append(char)
+ number = "" # reset number now that we have encountered a type
+ else:
+ types.append(char)
+ elif char in self.orders:
+ raise ValueError(
+ "The 'format' argument string contains a byte order specifier, '%s', in a location where it cannot be used; a byte order specifier can appear no more than once, and if included, it must be at the beginning of the format string!"
+ % (char)
+ )
+ else:
+ raise ValueError(
+ "The 'format' argument string contains a type specifier, '%s', that is not recognized; type specifiers must be one of the following values: '%s'!"
+ % (char, "', '".join(self.types))
+ )
+
+ values: list[Type] = []
+
+ # Iterate through the types, attempting to decode and cast each data type value
+ for type in types:
+ if isinstance(value := next(self.cast(type=type, order=order)), Type):
+ values.append(value)
+ else:
+ values.append(None)
+
+ return tuple(values)
+
+ @classmethod
+ def encode(
+ cls,
+ values: list[Type] | tuple[Type],
+ order: ByteOrder = ByteOrder.MSB,
+ ) -> BytesView:
+ """The encode class method provides support for encoding one or more Type class
+ instances to their underlying bytes values and concatenating those bytes to form
+ the input data for a BytesView class instance that can then be used to further
+ work with and manipulate the data as needed."""
+
+ if not isinstance(values, (list, tuple)):
+ raise TypeError(
+ "The 'values' argument must reference a list or tuple of Type instances!"
+ )
+
+ if not isinstance(order, ByteOrder):
+ raise TypeError(
+ "The 'order' argument, if specified, must reference a ByteOrder enumeration option!"
+ )
+
+ data: bytearray = bytearray()
+
+ for value in values:
+ if not isinstance(value, Type):
+ raise TypeError(
+ "All of the values provided to '%s.encode()' must be instances of Type!"
+ % (cls.__name__,)
+ )
+
+ data += value.encode(order=order)
+
+ return BytesView(data, order=order)
diff --git a/source/deliciousbytes/version.txt b/source/deliciousbytes/version.txt
index 7f20734..e6d5cb8 100644
--- a/source/deliciousbytes/version.txt
+++ b/source/deliciousbytes/version.txt
@@ -1 +1 @@
-1.0.1
\ No newline at end of file
+1.0.2
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
index a8e6b0b..e436803 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,7 +9,7 @@
import deliciousbytes
-def print_bytes_hex(data: bytes, prefix: bool = False):
+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]
)
diff --git a/tests/test_library.py b/tests/test_library.py
index a4ac38c..55675b6 100644
--- a/tests/test_library.py
+++ b/tests/test_library.py
@@ -1,8 +1,11 @@
import pytest
+import math
+import sys
from deliciousbytes import (
Encoding,
ByteOrder,
+ Type,
Int,
Int8,
Int16,
@@ -14,12 +17,25 @@
UInt32,
UInt64,
Char,
- SignedLong,
SignedChar,
+ UnsignedChar,
+ Short,
+ SignedShort,
+ UnsignedShort,
Long,
+ UnsignedLong,
SignedLong,
LongLong,
+ UnsignedLongLong,
SignedLongLong,
+ Size,
+ SignedSize,
+ UnsignedSize,
+ Float,
+ Float16,
+ Float32,
+ Float64,
+ Double,
Bytes,
Bytes8,
Bytes16,
@@ -28,42 +44,77 @@
Bytes128,
Bytes256,
String,
+ Unicode,
+ UTF8,
+ UTF16,
+ UTF32,
+ ASCII,
)
-from conftest import print_bytes_hex
+from conftest import print_hexbytes
+
+def test_byte_order_enumeration():
+ """Test the ByteOrder enumeration class."""
-def test_byte_order():
assert ByteOrder.MSB is ByteOrder.BigEndian
assert ByteOrder.MSB is ByteOrder.Motorolla
+ assert ByteOrder.MSB is ByteOrder.Big
assert ByteOrder.LSB is ByteOrder.LittleEndian
assert ByteOrder.LSB is ByteOrder.Intel
+ assert ByteOrder.LSB is ByteOrder.Little
+
+ if sys.byteorder == "big":
+ assert ByteOrder.Native is ByteOrder.MSB
+ elif sys.byteorder == "little":
+ assert ByteOrder.Native is ByteOrder.LSB
+
+
+def test_encoding_enumeration():
+ """Test the Encoding enumeration class."""
+
+ assert Encoding.ASCII == "ascii"
+ assert Encoding.Bytes == "bytes"
+ assert Encoding.Unicode == "utf-8"
+ assert Encoding.UTF8 == "utf-8"
+ assert Encoding.UTF16 == "utf-16"
+ assert Encoding.UTF32 == "utf-32"
def test_int():
+ """Test the Int data type which subclases int, Python's arbitrarily long int type."""
+
value: Int = Int(4000050)
- assert isinstance(value, int)
assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 0 # byte length, 0 means unbounded, only limited by memory
+ assert value.signed is True
assert value == 4000050
encoded: bytes = value.encode(order=ByteOrder.BigEndian)
assert isinstance(encoded, bytes)
- assert encoded == b"\x00\x00\x00\x00\x00\x3d\x09\x32"
+ assert encoded == b"\x3d\x09\x32"
encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
assert isinstance(encoded, bytes)
- assert encoded == b"\x32\x09\x3d\x00\x00\x00\x00\x00"
+ assert encoded == b"\x32\x09\x3d"
def test_int8():
+ """Test the Int8 data type which is a fixed 1-byte, 8-bit signed integer type."""
+
value: Int8 = Int8(127)
- assert isinstance(value, int)
- assert isinstance(value, Int)
assert isinstance(value, Int8)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 1 # byte length
+ assert value.signed is True
assert value == 127
@@ -77,11 +128,16 @@ def test_int8():
def test_int8_overflow():
+ """Test the Int8 data type which is a fixed 1-byte, 8-bit signed integer type."""
+
value: Int8 = Int8(129)
- assert isinstance(value, int)
- assert isinstance(value, Int)
assert isinstance(value, Int8)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 1 # byte length
+ assert value.signed is True
assert value == -127 # int8 129 overflows to -127
@@ -95,11 +151,16 @@ def test_int8_overflow():
def test_int16():
+ """Test the Int16 data type which is a fixed 2-byte, 16-bit signed integer type."""
+
value: Int16 = Int16(127)
- assert isinstance(value, int)
- assert isinstance(value, Int)
assert isinstance(value, Int16)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 2 # byte length
+ assert value.signed is True
assert value == 127
@@ -113,11 +174,16 @@ def test_int16():
def test_int32():
+ """Test the Int32 data type which is a fixed 4-byte, 32-bit signed integer type."""
+
value: Int32 = Int32(127)
- assert isinstance(value, int)
- assert isinstance(value, Int)
assert isinstance(value, Int32)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 4 # byte length
+ assert value.signed is True
assert value == 127
@@ -131,11 +197,16 @@ def test_int32():
def test_int64():
+ """Test the Int64 data type which is a fixed 8-byte, 64-bit signed integer type."""
+
value: Int64 = Int64(127)
- assert isinstance(value, int)
- assert isinstance(value, Int)
assert isinstance(value, Int64)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 8 # byte length
+ assert value.signed is True
assert value == 127
@@ -149,12 +220,17 @@ def test_int64():
def test_uint8():
+ """Test the UInt8 data type which is a fixed 1-byte, 8-bit unsigned integer type."""
+
value: UInt8 = UInt8(127)
- assert isinstance(value, int)
- assert isinstance(value, Int)
- assert isinstance(value, UInt)
assert isinstance(value, UInt8)
+ assert isinstance(value, UInt)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 1 # byte length
+ assert value.signed is False
assert value == 127
@@ -168,12 +244,17 @@ def test_uint8():
def test_uint8_overflow():
+ """Test the UInt8 data type which is a fixed 1-byte, 8-bit unsigned integer type."""
+
value: UInt8 = UInt8(256)
- assert isinstance(value, int)
- assert isinstance(value, Int)
- assert isinstance(value, UInt)
assert isinstance(value, UInt8)
+ assert isinstance(value, UInt)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 1 # byte length
+ assert value.signed is False
assert value == 0 # uint8 256 overflows to 0
@@ -187,12 +268,17 @@ def test_uint8_overflow():
def test_uint16():
+ """Test the UInt16 data type which is a fixed 2-byte, 16-bit unsigned integer type."""
+
value: UInt16 = UInt16(127)
- assert isinstance(value, int)
- assert isinstance(value, Int)
- assert isinstance(value, UInt)
assert isinstance(value, UInt16)
+ assert isinstance(value, UInt)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 2 # byte length
+ assert value.signed is False
assert value == 127
@@ -206,12 +292,17 @@ def test_uint16():
def test_uint32():
+ """Test the UInt32 data type which is a fixed 4-byte, 32-bit unsigned integer type."""
+
value: UInt32 = UInt32(4000050)
- assert isinstance(value, int)
- assert isinstance(value, Int)
- assert isinstance(value, UInt)
assert isinstance(value, UInt32)
+ assert isinstance(value, UInt)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 4 # byte length
+ assert value.signed is False
encoded: bytes = value.encode(order=ByteOrder.BigEndian)
assert isinstance(encoded, bytes)
@@ -222,19 +313,506 @@ def test_uint32():
assert encoded == b"\x32\x09\x3d\x00"
-def test_bytes64():
- value: UInt32 = UInt32(4000050)
+def test_uint64():
+ """Test the UInt64 data type which is a fixed 8-byte, 64-bit unsigned integer type."""
+
+ value: UInt64 = UInt64(4000050)
+ assert isinstance(value, UInt64)
+ assert isinstance(value, UInt)
+ assert isinstance(value, Int)
assert isinstance(value, int)
+
+ assert value.length == 8 # byte length
+ assert value.signed is False
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x00\x00\x00\x00\x00\x3d\x09\x32"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x32\x09\x3d\x00\x00\x00\x00\x00"
+
+
+def test_char():
+ """Test the Char data type which is a fixed 1-byte, 8-bit unsigned integer type."""
+
+ value: Char = Char("a")
+
+ assert isinstance(value, Char)
assert isinstance(value, Int)
- assert isinstance(value, UInt)
+ assert isinstance(value, int)
+
+ assert value.length == 1 # byte length
+ assert value.signed is False
+
+ assert value == 97
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"a"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"a"
+
+
+def test_signed_char():
+ """Test the SignedChar data type which is a fixed 1-byte, 8-bit signed integer type."""
+
+ value: SignedChar = SignedChar("a")
+
+ assert isinstance(value, SignedChar)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 1 # byte length
+ assert value.signed is True
+
+ assert value == 97
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"a"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"a"
+
+
+def test_unsigned_char():
+ """Test the UnsignedChar data type which is a fixed 1-byte, 8-bit unsigned integer type."""
+
+ value: UnsignedChar = UnsignedChar("a")
+
+ assert isinstance(value, UnsignedChar)
+ assert isinstance(value, Char)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 1 # byte length
+ assert value.signed is False
+
+ assert value == 97
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"a"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"a"
+
+
+def test_short():
+ """Test the Short data type which is a fixed 2-byte, 16-bit signed integer type."""
+
+ value: Short = Short(127)
+
+ assert isinstance(value, Short)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 2 # byte length
+ assert value.signed is True
+
+ assert value == 127
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x00\x7f"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x7f\x00"
+
+
+def test_unsigned_short():
+ """Test the UnsignedShort data type which is a fixed 2-byte, 16-bit unsigned integer type."""
+
+ value: UnsignedShort = UnsignedShort(127)
+
+ assert isinstance(value, UnsignedShort)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 2 # byte length
+ assert value.signed is False
+
+ assert value == 127
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x00\x7f"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x7f\x00"
+
+
+def test_signed_short():
+ """Test the SignedShort data type which is a fixed 2-byte, 16-bit signed integer type."""
+
+ value: SignedShort = SignedShort(127)
+
+ assert isinstance(value, SignedShort)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 2 # byte length
+ assert value.signed is True
+
+ assert value == 127
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x00\x7f"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x7f\x00"
+
+
+def test_long():
+ """Test the Long data type which is a fixed 4-byte, 32-bit signed integer type."""
+
+ value: Long = Long(127)
+
+ assert isinstance(value, Long)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 4 # byte length
+ assert value.signed is True
+
+ assert value == 127
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x00\x00\x00\x7f"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x7f\x00\x00\x00"
+
+
+def test_unsigned_long():
+ """Test the UnsignedLong data type which is a fixed 4-byte, 32-bit unsigned integer type."""
+
+ value: UnsignedLong = UnsignedLong(127)
+
+ assert isinstance(value, UnsignedLong)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 4 # byte length
+ assert value.signed is False
+
+ assert value == 127
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x00\x00\x00\x7f"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x7f\x00\x00\x00"
+
+
+def test_signed_long():
+ """Test the SignedLong data type which is a fixed 4-byte, 32-bit signed integer type."""
+
+ value: SignedLong = SignedLong(127)
+
+ assert isinstance(value, SignedLong)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 4 # byte length
+ assert value.signed is True
+
+ assert value == 127
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x00\x00\x00\x7f"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x7f\x00\x00\x00"
+
+
+def test_long_long():
+ """Test the LongLong data type which is a fixed 8-byte, 64-bit signed integer type."""
+
+ value: LongLong = LongLong(127)
+
+ assert isinstance(value, LongLong)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 8 # byte length
+ assert value.signed is True
+
+ assert value == 127
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x00\x00\x00\x00\x00\x00\x00\x7f"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x7f\x00\x00\x00\x00\x00\x00\x00"
+
+
+def test_unsigned_long_long():
+ """Test the UnsignedLongLong data type which is a fixed 8-byte, 64-bit unsigned integer type."""
+
+ value: UnsignedLongLong = UnsignedLongLong(127)
+
+ assert isinstance(value, UnsignedLongLong)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 8 # byte length
+ assert value.signed is False
+
+ assert value == 127
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x00\x00\x00\x00\x00\x00\x00\x7f"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x7f\x00\x00\x00\x00\x00\x00\x00"
+
+
+def test_signed_long_long():
+ """Test the SignedLongLong data type which is a fixed 8-byte, 64-bit signed integer type."""
+
+ value: SignedLongLong = SignedLongLong(127)
+
+ assert isinstance(value, SignedLongLong)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length == 8 # byte length
+ assert value.signed is True
+
+ assert value == 127
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x00\x00\x00\x00\x00\x00\x00\x7f"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x7f\x00\x00\x00\x00\x00\x00\x00"
+
+
+def test_size():
+ """Test the Size data type which is a variable length unsigned integer type."""
+
+ value: Size = Size(127)
+
+ assert isinstance(value, Size)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length >= 1 # byte length
+ assert value.signed is False
+
+ assert value == 127
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded.endswith(b"\x00\x00\x00\x7f")
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded.startswith(b"\x7f\x00\x00\x00")
+
+
+def test_signed_size():
+ """Test the SignedSize data type which is a variable length signed integer type."""
+
+ value: SignedSize = SignedSize(127)
+
+ assert isinstance(value, SignedSize)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length >= 1 # byte length
+ assert value.signed is True
+
+ assert value == 127
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded.endswith(b"\x00\x00\x00\x7f")
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded.startswith(b"\x7f\x00\x00\x00")
+
+
+def test_unsigned_size():
+ """Test the UnsignedSize data type which is a variable length unsigned integer type."""
+
+ value: UnsignedSize = UnsignedSize(127)
+
+ assert isinstance(value, UnsignedSize)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
+
+ assert value.length >= 1 # byte length
+ assert value.signed is False
+
+ assert value == 127
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded.endswith(b"\x00\x00\x00\x7f")
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded.startswith(b"\x7f\x00\x00\x00")
+
+
+def test_float():
+ """Test the Float data type which is a 8-byte, 64-bit floating point type."""
+
+ value: Float = Float(127.987)
+
+ assert isinstance(value, Float)
+ assert isinstance(value, float)
+
+ assert value.length == 8 # byte length
+ assert value.signed is True
+
+ # Compare using math.isclose() due to float precision variance between systems
+ assert math.isclose(value, 127.987)
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x40\x5f\xff\x2b\x02\x0c\x49\xba"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\xba\x49\x0c\x02\x2b\xff\x5f\x40"
+
+
+def test_float16():
+ """Test the Float16 data type which is a 2-byte, 16-bit floating point type."""
+
+ value: Float16 = Float16(127.987)
+
+ assert isinstance(value, Float16)
+ assert isinstance(value, Float)
+ assert isinstance(value, float)
+
+ assert value.length == 2 # byte length
+ assert value.signed is True
+
+ # Note: 16 bit float looses some precision, will rounds to 128.0
+ assert math.isclose(value, 127.987)
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x58\x00"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x00\x58"
+
+ decoded: Float16 = Float16.decode(encoded, order=ByteOrder.LittleEndian)
+ assert isinstance(decoded, Float16)
+ assert isinstance(decoded, Float)
+ assert isinstance(decoded, float)
+
+ # Note: 16 bit float looses some precision, so 127.987 rounds to 128.0
+ assert math.isclose(decoded, 128.0)
+
+
+def test_float32():
+ """Test the Float32 data type which is a 4-byte, 32-bit floating point type."""
+
+ value: Float32 = Float32(127.987)
+
+ assert isinstance(value, Float32)
+ assert isinstance(value, Float)
+ assert isinstance(value, float)
+
+ assert value.length == 4 # byte length
+ assert value.signed is True
+
+ # Note: 32 bit float looses some precision, so 127.987 rounds to 127.98699951171875
+ assert math.isclose(value, 127.987)
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x42\xff\xf9\x58"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x58\xf9\xff\x42"
+
+ decoded: Float32 = Float32.decode(encoded, order=ByteOrder.LittleEndian)
+ assert isinstance(decoded, Float32)
+ assert isinstance(decoded, Float)
+ assert isinstance(decoded, float)
+
+ # Note: 32 bit float looses some precision, so 127.987 rounds to 127.98699951171875
+ assert math.isclose(decoded, 127.98699951171875)
+
+
+def test_float64():
+ """Test the Float64 data type which is a 8-byte, 64-bit floating point type."""
+
+ value: Float64 = Float64(127.987)
+
+ assert isinstance(value, Float64)
+ assert isinstance(value, Float)
+ assert isinstance(value, float)
+
+ assert value.length == 8 # byte length
+ assert value.signed is True
+
+ assert math.isclose(value, 127.987)
+
+ encoded: bytes = value.encode(order=ByteOrder.BigEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x40\x5f\xff\x2b\x02\x0c\x49\xba"
+
+ encoded: bytes = value.encode(order=ByteOrder.LittleEndian)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\xba\x49\x0c\x02\x2b\xff\x5f\x40"
+
+ decoded: Float64 = Float64.decode(encoded, order=ByteOrder.LittleEndian)
+ assert isinstance(decoded, Float64)
+ assert isinstance(decoded, Float)
+ assert isinstance(decoded, float)
+
+ assert math.isclose(decoded, 127.987)
+
+
+def test_bytes64():
+ """Test the Bytes64 data type which is a fixed 8-byte, 64-bit bytes type."""
+
+ value: UInt32 = UInt32(4000050)
+
assert isinstance(value, UInt32)
+ assert isinstance(value, UInt)
+ assert isinstance(value, Int)
+ assert isinstance(value, int)
value: Bytes64 = Bytes64(bytearray([byte for byte in bytes(value)]))
- assert isinstance(value, bytes)
- assert isinstance(value, Bytes)
assert isinstance(value, Bytes64)
+ assert isinstance(value, Bytes)
+ assert isinstance(value, bytes)
encoded: bytes = value.encode(order=ByteOrder.MSB)
assert isinstance(encoded, bytes)
@@ -246,6 +824,8 @@ def test_bytes64():
def test_string():
+ """Test the String data type which is unbounded and defaults to UTF-8 encoding."""
+
uncoded: String = String("hello")
assert isinstance(uncoded, String)
@@ -253,17 +833,193 @@ def test_string():
# As String is a subclass of 'str' we can compare values directly
assert uncoded == "hello"
+ assert uncoded.encoding is Encoding.UTF8
encoded: bytes = uncoded.encode(order=ByteOrder.MSB)
-
assert isinstance(encoded, bytes)
- assert encoded == b"hello"
+ assert encoded == b"\x68\x65\x6c\x6c\x6f"
- decoded = String.decode(b"hello")
+ encoded: bytes = uncoded.encode(order=ByteOrder.LSB)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x6f\x6c\x6c\x65\x68"
+ decoded = String.decode(b"\x68\x65\x6c\x6c\x6f", order=ByteOrder.MSB)
assert isinstance(decoded, String)
assert isinstance(decoded, str)
# As String is a subclass of 'str' we can compare values directly
assert decoded == "hello"
- assert decoded.encode() == b"hello"
+ assert decoded.encode(order=ByteOrder.MSB) == b"\x68\x65\x6c\x6c\x6f"
+
+
+def test_unicode():
+ """Test the Unicode data type which is unbounded and defaults to UTF-8 encoding."""
+
+ uncoded: Unicode = Unicode("hello")
+
+ assert isinstance(uncoded, Unicode)
+ assert isinstance(uncoded, String)
+ assert isinstance(uncoded, str)
+
+ # As Unicode is ultimately a subclass of 'str' we can compare values directly
+ assert uncoded == "hello"
+ assert uncoded.encoding is Encoding.UTF8
+
+ encoded: bytes = uncoded.encode(order=ByteOrder.MSB)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x68\x65\x6c\x6c\x6f"
+
+ encoded: bytes = uncoded.encode(order=ByteOrder.LSB)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x6f\x6c\x6c\x65\x68"
+
+ decoded = Unicode.decode(b"\x68\x65\x6c\x6c\x6f", order=ByteOrder.MSB)
+ assert isinstance(decoded, Unicode)
+ assert isinstance(decoded, String)
+ assert isinstance(decoded, str)
+
+ # As Unicode is ultimately a subclass of 'str' we can compare values directly
+ assert decoded == "hello"
+ assert decoded.encode(order=ByteOrder.MSB) == b"hello"
+
+
+def test_utf8():
+ """Test the UTF8 data type which is unbounded and uses UTF-8 encoding."""
+
+ uncoded: UTF8 = UTF8("hello")
+
+ assert isinstance(uncoded, UTF8)
+ assert isinstance(uncoded, Unicode)
+ assert isinstance(uncoded, String)
+ assert isinstance(uncoded, str)
+
+ # As UTF8 is ultimately a subclass of 'str' we can compare values directly
+ assert uncoded == "hello"
+ assert uncoded.encoding is Encoding.UTF8
+
+ encoded: bytes = uncoded.encode(order=ByteOrder.MSB)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x68\x65\x6c\x6c\x6f"
+
+ encoded: bytes = uncoded.encode(order=ByteOrder.LSB)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x6f\x6c\x6c\x65\x68"
+
+ decoded = UTF8.decode(b"\x68\x65\x6c\x6c\x6f", order=ByteOrder.MSB)
+ assert isinstance(decoded, UTF8)
+ assert isinstance(decoded, Unicode)
+ assert isinstance(decoded, String)
+ assert isinstance(decoded, str)
+
+ # As UTF8 is ultimately a subclass of 'str' we can compare values directly
+ assert decoded == "hello"
+ assert decoded.encode(order=ByteOrder.MSB) == b"\x68\x65\x6c\x6c\x6f"
+
+
+def test_utf16():
+ """Test the UTF16 data type which is unbounded and uses UTF-16 encoding."""
+
+ uncoded: UTF16 = UTF16("hello")
+
+ assert isinstance(uncoded, UTF16)
+ assert isinstance(uncoded, Unicode)
+ assert isinstance(uncoded, String)
+ assert isinstance(uncoded, str)
+
+ # As UTF16 is ultimately a subclass of 'str' we can compare values directly
+ assert uncoded == "hello"
+ assert uncoded.encoding is Encoding.UTF16
+
+ encoded: bytes = uncoded.encode(order=ByteOrder.MSB)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\xff\xfe\x68\x00\x65\x00\x6c\x00\x6c\x00\x6f\x00"
+
+ encoded: bytes = uncoded.encode(order=ByteOrder.LSB)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x00\x6f\x00\x6c\x00\x6c\x00\x65\x00\x68\xfe\xff"
+
+ decoded = UTF16.decode(encoded, order=ByteOrder.LSB)
+ assert isinstance(decoded, UTF16)
+ assert isinstance(decoded, Unicode)
+ assert isinstance(decoded, String)
+ assert isinstance(decoded, str)
+
+ # As UTF16 is ultimately a subclass of 'str' we can compare values directly
+ assert decoded == "hello"
+ assert (
+ decoded.encode(order=ByteOrder.MSB)
+ == b"\xff\xfe\x68\x00\x65\x00\x6c\x00\x6c\x00\x6f\x00"
+ )
+
+
+def test_utf32():
+ """Test the UTF32 data type which is unbounded and uses UTF-32 encoding."""
+
+ uncoded: UTF32 = UTF32("hello")
+
+ assert isinstance(uncoded, UTF32)
+ assert isinstance(uncoded, Unicode)
+ assert isinstance(uncoded, String)
+ assert isinstance(uncoded, str)
+
+ # As UTF32 is ultimately a subclass of 'str' we can compare values directly
+ assert uncoded == "hello"
+ assert uncoded.encoding is Encoding.UTF32
+
+ encoded: bytes = uncoded.encode(order=ByteOrder.MSB)
+ assert isinstance(encoded, bytes)
+ assert (
+ encoded
+ == b"\xff\xfe\x00\x00\x68\x00\x00\x00\x65\x00\x00\x00\x6c\x00\x00\x00\x6c\x00\x00\x00\x6f\x00\x00\x00"
+ )
+
+ encoded: bytes = uncoded.encode(order=ByteOrder.LSB)
+ assert isinstance(encoded, bytes)
+ assert (
+ encoded
+ == b"\x00\x00\x00\x6f\x00\x00\x00\x6c\x00\x00\x00\x6c\x00\x00\x00\x65\x00\x00\x00\x68\x00\x00\xfe\xff"
+ )
+
+ decoded = UTF32.decode(encoded, order=ByteOrder.LSB)
+ assert isinstance(decoded, UTF32)
+ assert isinstance(decoded, Unicode)
+ assert isinstance(decoded, String)
+ assert isinstance(decoded, str)
+
+ # As UTF32 is ultimately a subclass of 'str' we can compare values directly
+ assert decoded == "hello"
+ assert (
+ decoded.encode(order=ByteOrder.MSB)
+ == b"\xff\xfe\x00\x00\x68\x00\x00\x00\x65\x00\x00\x00\x6c\x00\x00\x00\x6c\x00\x00\x00\x6f\x00\x00\x00"
+ )
+
+
+def test_ascii():
+ """Test the ASCII data type which is unbounded and uses ASCII encoding."""
+
+ uncoded: ASCII = ASCII("hello")
+
+ assert isinstance(uncoded, ASCII)
+ assert isinstance(uncoded, String)
+ assert isinstance(uncoded, str)
+
+ # As ASCII is ultimately a subclass of 'str' we can compare values directly
+ assert uncoded == "hello"
+ assert uncoded.encoding is Encoding.ASCII
+
+ encoded: bytes = uncoded.encode(order=ByteOrder.MSB)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x68\x65\x6c\x6c\x6f"
+
+ encoded: bytes = uncoded.encode(order=ByteOrder.LSB)
+ assert isinstance(encoded, bytes)
+ assert encoded == b"\x6f\x6c\x6c\x65\x68"
+
+ decoded = ASCII.decode(encoded, order=ByteOrder.LSB)
+ assert isinstance(decoded, ASCII)
+ assert isinstance(decoded, String)
+ assert isinstance(decoded, str)
+
+ # As ASCII is ultimately a subclass of 'str' we can compare values directly
+ assert decoded == "hello"
+ assert decoded.encode(order=ByteOrder.MSB) == b"\x68\x65\x6c\x6c\x6f"
diff --git a/tests/test_view.py b/tests/test_view.py
new file mode 100644
index 0000000..d78f3d8
--- /dev/null
+++ b/tests/test_view.py
@@ -0,0 +1,300 @@
+import pytest
+
+from deliciousbytes import (
+ ByteOrder,
+ BytesView,
+ Short,
+ Long,
+ Type,
+)
+
+
+@pytest.fixture(scope="module", name="data")
+def test_data_fixture() -> bytes:
+ """Assemble some test raw bytes data to work with throughout the unit tests."""
+
+ return b"\x00\x01\x00\x02\x00\x03\x00\x04"
+
+
+@pytest.fixture(scope="module", name="view")
+def test_bytes_view_fixture(data: bytes) -> BytesView:
+ """Create a BytesView class instance to work with throughout the unit tests."""
+
+ # Ensure that the data is bytes or bytearray
+ assert isinstance(data, (bytes, bytearray))
+
+ # Ensure that the data has the expected length
+ assert len(data) == 8
+
+ # Here we create a BytesView class instance with the assigned data and an initial
+ # split length of 2, so when iterating the data will be split into groups of 2 bytes
+ return BytesView(data, split=2)
+
+
+def test_bytesview_initialization(data: bytes, view: BytesView):
+ """Test the BytesView class initialization was successful."""
+
+ assert isinstance(view, BytesView)
+
+ assert view.data == data
+ assert view.splits == 2
+
+
+def test_bytesview_length(view: BytesView):
+ """Test that the BytesView reports its current length correctly."""
+
+ # The length reflects the current length of the data as divided by the split size
+ # This is the number of items that can iterated over in the view where the maximum
+ # usable index for iteration or item level access is the reported length minus 1.
+ assert len(view) == 4
+
+
+def test_bytesview_iteration_via_for_in_loop(view: BytesView):
+ """Test that the BytesView can be iterated over correctly using a for...in loop."""
+
+ # Iterate over the values using normal iterator semantics such as for/enumerate
+ for index, val in enumerate(view):
+ if index == 0:
+ assert val == b"\x00\x01"
+ elif index == 1:
+ assert val == b"\x00\x02"
+ elif index == 2:
+ assert val == b"\x00\x03"
+ elif index == 3:
+ assert val == b"\x00\x04"
+
+
+def test_bytesview_item_access(view: BytesView):
+ """Test that the BytesView byte groups can be accessed via item access semantics."""
+
+ # Access individual groups of bytes (based on the split size) using item access
+ assert view[0] == b"\x00\x01"
+ assert view[1] == b"\x00\x02"
+ assert view[2] == b"\x00\x03"
+ assert view[3] == b"\x00\x04"
+
+
+def test_bytesview_item_access_slicing(view: BytesView):
+ """Test that the BytesView byte groups can be accessed via slicing semantics."""
+
+ # Note: When slicing access is used, the current split length is ignored
+
+ # Test obtaining bytes from 1 until 4 (i.e. bytes 1, 2, 3)
+ assert view[1:4] == b"\x01\x00\x02"
+
+ # Test obtaining bytes from 0 until 4 (i.e. bytes 0, 1, 2, 3)
+ assert view[0:4:+1] == b"\x00\x01\x00\x02"
+
+ # Test obtaining bytes from 0 until 8, stepping 2 bytes each time
+ assert view[0:8:+2] == b"\x00\x00\x00\x00"
+
+ # Test obtaining bytes from 1 until 8, stepping 2 bytes each time
+ assert view[1:8:+2] == b"\x01\x02\x03\x04"
+
+ # Test obtaining bytes from 0 until 8, stepping -2 bytes each time, i.e. reversed
+ assert view[1:8:-2] == b"\x04\x03\x02\x01"
+
+ # Test obtaining bytes from 0 until 4, stepping -1 bytes each time, i.e. reversed
+ assert view[0:4:-1] == b"\x02\x00\x01\x00"
+
+
+def test_bytesview_changing_split_size_after_initialization(view: BytesView):
+ """Test that the BytesView split size can be changed after initialization."""
+
+ # Change the split size at any point; the last split size will be remembered (!)
+ for index, val in enumerate(view.split(4)):
+ if index == 0:
+ assert val == b"\x00\x01\x00\x02"
+ elif index == 1:
+ assert val == b"\x00\x03\x00\x04"
+
+ # Note how the group size changed based on the split size set most recently
+ assert view[0] == b"\x00\x01\x00\x02"
+ assert view[1] == b"\x00\x03\x00\x04"
+
+
+def test_bytesview_casting(view: BytesView):
+ """Test that the BytesView can cast groups to the defined data type."""
+
+ # Cast the values from raw bytes to the defined type; note that casting implies an
+ # associated split size as each type cast requires the relevant number of bytes for
+ # decoding to the defined type; byte order can be specified using struct shorthand
+ for index, val in enumerate(view.cast(">h")):
+ if index == 0:
+ assert val == 1
+ elif index == 1:
+ assert val == 2
+ elif index == 2:
+ assert val == 3
+ elif index == 3:
+ assert val == 4
+
+
+def test_bytesview_tell(view: BytesView):
+ """Test that the BytesView tell method reports the expected index position."""
+
+ # The current index position can be determined by using the 'tell' method; as the
+ # above iterator just completed iterating through 8 bytes of data, index should be 8
+ assert view.tell() == 8
+
+
+def test_bytesview_seek_individual_bytes(view: BytesView):
+ """Test that the BytesView seek method changes the index position as expected."""
+
+ # Adjust the split size to 1 to test setting the seek position to individual bytes
+ view.split(1)
+
+ # The current index position can be adjusted using the 'seek' method; if an split
+ # length is set, then the index will be set to the defined multiple of split; the
+ # 'seek' method also returns a reference to 'self' so calls can be chained; notice
+ # that the index positions reported by 'tell' are multiples of the split size:
+ assert view.seek(0).tell() == 0
+ assert view.seek(1).tell() == 1
+ assert view.seek(2).tell() == 2
+ assert view.seek(3).tell() == 3
+ assert view.seek(4).tell() == 4
+ assert view.seek(5).tell() == 5
+ assert view.seek(6).tell() == 6
+ assert view.seek(7).tell() == 7
+
+
+def test_bytesview_seek_groups_of_bytes(view: BytesView):
+ """Test that the BytesView seek method changes the index position as expected."""
+
+ # Adjust the split size to 2 to test setting the seek position to groups of bytes
+ view.split(2)
+
+ # The current index position can be adjusted using the 'seek' method; if an split
+ # length is set, then the index will be set to the defined multiple of split; the
+ # 'seek' method also returns an reference to 'self' to calls can be chained; notice
+ # that the index positions reported by 'tell' are multiples of the split size:
+ assert view.seek(0).tell() == 0
+ assert view.seek(1).tell() == 2
+ assert view.seek(2).tell() == 4
+ assert view.seek(3).tell() == 6
+
+
+def test_bytesview_next_iteration(view: BytesView):
+ """Test that the BytesView next method returns the expected view item."""
+
+ # Create a new iterator which resets the iteration position to zero (0)
+ view = iter(view)
+
+ # Ensure that the iterator index was reset to 0
+ assert view.tell() == 0
+
+ # The 'next' method may also be used to obtain the next item from the view, either
+ # as raw bytes, or decoded into the specified type; call .next() for each item in
+ # the view, specifying how each item should be decoded.
+
+ # Test obtaining the next (in this case the first) item as raw bytes
+ assert view.next() == b"\x00\x01"
+
+ # Test obtaining the next (in this case the second) item as a short integer (>h)
+ assert view.next(">h") == 2
+
+ # Test obtaining the next (in this case the third) item as a short integer (>h)
+ assert view.next(">h") == 3
+
+ # Test obtaining the next (in this case the fourth) item as a short integer (>h)
+ assert view.next(">h") == 4
+
+ # If .next() is called more times than there are items in the view, it returns None;
+ # if the iterator or its index is reset, .next() will be able to yield values again:
+ assert view.next(">h") is None
+
+
+def test_bytesview_decode(view: BytesView):
+ """Test that the BytesView decode method returns the expected decoded items."""
+
+ decoded: tuple[Type] = view.decode(">hhhh")
+
+ assert isinstance(decoded, tuple)
+
+ assert len(decoded) == 4
+
+ assert decoded == (1, 2, 3, 4)
+
+ assert isinstance(decoded[0], Short)
+ assert isinstance(decoded[0], int)
+ assert decoded[0] == 1
+
+ assert isinstance(decoded[1], Short)
+ assert isinstance(decoded[1], int)
+ assert decoded[1] == 2
+
+ assert isinstance(decoded[2], Short)
+ assert isinstance(decoded[2], int)
+ assert decoded[2] == 3
+
+ assert isinstance(decoded[3], Short)
+ assert isinstance(decoded[3], int)
+ assert decoded[3] == 4
+
+
+def test_bytesview_decode_numeric_format_string(view: BytesView):
+ """Test that the BytesView decode method returns the expected decoded items."""
+
+ decoded: tuple[Type] = view.decode(">2h h 1h")
+
+ assert isinstance(decoded, tuple)
+
+ assert len(decoded) == 4
+
+ assert decoded == (1, 2, 3, 4)
+
+
+def test_bytesview_encode(view: BytesView):
+ """Test that the BytesView decode method returns the expected decoded items."""
+
+ # Assemble some values for testing, each a subclass of Type
+ values: list[Type] = [
+ Short(7), # 2-byte, 16-bits
+ Short(8), # 2-byte, 16-bits
+ Long(9), # 4-byte, 32-bits
+ ]
+
+ # Encode the values into a BytesView class instance
+ encoded: BytesView = BytesView.encode(values, order=ByteOrder.LSB)
+
+ # Ensure that the BytesView class instance was created correctly
+ assert isinstance(encoded, BytesView)
+
+ # Ensure that the encoded length of the data is as expected
+ assert len(encoded) == 8
+
+ # Ensure that the raw data looks as we would expect it to
+ assert encoded[0:2] == b"\x07\x00" # 7 (as short, 2-bytes, LSB)
+ assert encoded[2:4] == b"\x08\x00" # 8 (as short, 2-bytes, LSB)
+ assert encoded[4:8] == b"\x09\x00\x00\x00" # 9 (as long, 4-bytes, LSB)
+
+ # Now ensure that the values can be decoded correctly as well
+ decoded: tuple[Type] = encoded.decode("h h l", order=ByteOrder.LSB)
+
+ # Ensure that the decoded values were returned as a tuple
+ assert isinstance(decoded, tuple)
+
+ # Ensure that the tuple contains the expected number of entries
+ assert len(decoded) == 3
+
+ # Ensure that each value held in the tuple is a deliciousbytes.Type subclass
+ for value in decoded:
+ assert isinstance(value, Type)
+
+ # Ensure that the decoded values are as expected
+ assert decoded == (7, 8, 9)
+
+ # Ensure that the first value has the expected data type and value
+ assert isinstance(decoded[0], Short)
+ assert isinstance(decoded[0], int)
+ assert decoded[0] == 7
+
+ # Ensure that the second value has the expected data type and value
+ assert isinstance(decoded[1], Short)
+ assert isinstance(decoded[1], int)
+ assert decoded[1] == 8
+
+ # Ensure that the third value has the expected data type and value
+ assert isinstance(decoded[2], Long)
+ assert isinstance(decoded[2], int)
+ assert decoded[2] == 9