Skip to content

Commit

Permalink
Merge pull request #8 from AndrewSpittlemeister/QoL-update
Browse files Browse the repository at this point in the history
Quality of Life Update
  • Loading branch information
AndrewSpittlemeister authored Jul 31, 2024
2 parents 3651b2b + 4071271 commit 300d089
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 1,431 deletions.
7 changes: 3 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11"]
python-version: ["3.10", "3.11", "3.12"]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
Expand All @@ -24,10 +24,9 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
allow-prereleases: true
- run: poetry install
- run: poetry install --with=dev
- run: poetry run pylint --max-line-length=100 bytechomp --disable=duplicate-code
- run: poetry run black --verbose --check --diff --line-length=100 bytechomp
- run: poetry run mypy bytechomp
- run: poetry run mypy --strict bytechomp
- run: poetry run pytest
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,5 @@ dmypy.json
# custom
/tmp
/.vscode
*.sqlite
*.sqlite
/poetry.lock
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

[![ci](https://github.com/AndrewSpittlemeister/bytechomp/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/AndrewSpittlemeister/bytechomp/actions/workflows/ci.yml)
[![PyPI Version](https://img.shields.io/pypi/v/bytechomp.svg)](https://pypi.org/project/bytechomp/)
[![Python Versions](https://img.shields.io/pypi/pyversions/bytechomp.svg)](https://pypi.org/project/bytechomp/)
![Lines of Code](https://tokei.rs/b1/github/AndrewSpittlemeister/bytechomp?category=code)
[![Python Versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue)](https://pypi.org/project/bytechomp/)

> *A pure python, declarative custom binary protocol parser & generator using dataclasses and type hinting.*
Expand Down Expand Up @@ -88,7 +87,7 @@ serialized_struct: bytes = serialize(my_struct)
```

## Supported Type Fields
Fields on the dataclasses can be integers, floats, strings, bytes, lists, or other dataclasses. Python-native `int` and `float` represent 64-bit variants. Other sizes can be imported from `bytechomp`:
Fields on the dataclasses can be integers, floats, bytes, lists, or other dataclasses. Python-native `int` and `float` represent 64-bit variants. Other sizes can be imported from `bytechomp`:

```python
from bytechomp.datatypes import (
Expand Down
4 changes: 2 additions & 2 deletions bytechomp/basic_parsing_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
from bytechomp.datatypes.lookups import ELEMENTARY_TYPE


@dataclass
@dataclass(slots=True)
class BasicParsingElement:
"""Describes a node in the type tree."""

parsing_type: ELEMENTARY_TYPE | bytes # type: ignore
parsing_type: ELEMENTARY_TYPE | bytes
python_type: type | None
parser_tag: str
length: int
Expand Down
78 changes: 29 additions & 49 deletions bytechomp/data_descriptor.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"""
bytechomp.data_descriptor
"""
# pylint: disable=broad-exception-raised

from __future__ import annotations
from typing import Annotated, Any, get_origin, get_args
from typing import Annotated, Union, Any, get_origin, get_args
from dataclasses import is_dataclass, fields, MISSING
from collections import OrderedDict
import inspect
Expand All @@ -17,25 +16,25 @@
)
from bytechomp.basic_parsing_element import BasicParsingElement

TypeTree = OrderedDict[
str,
Union[type, BasicParsingElement, list[BasicParsingElement], list["TypeTree"], "TypeTree"],
]

def build_data_description(
datatype: type,
) -> OrderedDict[str, BasicParsingElement | type | list | OrderedDict]:

def build_data_description(datatype: type) -> TypeTree:
"""Uses reflection on the provided type to provide a tokenized type tree.
Args:
datatype (type): Type object for the user-defined dataclass.
Returns:
OrderedDict[str, BasicParsingElement | list | OrderedDict]: Type tree of BasicParsingElement
nodes.
TypeTree: Type tree of BasicParsingElement nodes.
"""

# pylint: disable=too-many-branches
# pylint: disable=duplicate-code

object_description: OrderedDict[
str, BasicParsingElement | list | OrderedDict | type
] = OrderedDict()
object_description: TypeTree = OrderedDict()
object_description["__struct_type__"] = datatype

for field in fields(datatype):
Expand All @@ -49,21 +48,21 @@ def build_data_description(
)
elif inspect.isclass(field.type) and is_dataclass(field.type):
if field.default != MISSING:
raise Exception(f"cannot have default value on nested types (field: {field.name})")
raise TypeError(f"cannot have default value on nested types (field: {field.name})")
object_description[field.name] = build_data_description(field.type)
elif get_origin(field.type) == Annotated:
args = get_args(field.type)

if len(args) != 2:
raise Exception(
raise TypeError(
f"annotated value should only have two arguments (field: {field.name})"
)

arg_type = args[0]
length = args[1]

if not isinstance(length, int):
raise Exception("second annotated argument must be an integer to denote length")
raise TypeError("second annotated argument must be an integer to denote length")

# # deal with string type
# if arg_type == str:
Expand Down Expand Up @@ -91,7 +90,7 @@ def build_data_description(
list_type_args = get_args(arg_type)

if len(list_type_args) != 1:
raise Exception(
raise TypeError(
f"list must contain only one kind of data type (field: {field.name})"
)

Expand All @@ -109,34 +108,25 @@ def build_data_description(
elif inspect.isclass(list_type) and is_dataclass(list_type):
object_description[field.name] = [build_data_description(list_type)] * length
else:
raise Exception(f"unsupported list type: {list_type} (field: {field.name})")
raise TypeError(f"unsupported list type: {list_type} (field: {field.name})")

else:
raise Exception(f"unsupported annotated type: {arg_type} (field: {field.name})")
raise TypeError(f"unsupported annotated type: {arg_type} (field: {field.name})")
elif field.type in [list, bytes]:
raise Exception(
raise TypeError(
f"annotation needed for list/bytes (length required, field: {field.name})"
)
else:
raise Exception(f"unsupported data type ({field.type}) on field {field.name}")
raise TypeError(f"unsupported data type ({field.type}) on field {field.name}")

return object_description


def build_data_pattern(
description: OrderedDict[
str,
BasicParsingElement | type | list[BasicParsingElement | OrderedDict] | OrderedDict,
]
) -> str:
def build_data_pattern(description: TypeTree) -> str:
"""Determines a packed data representation using the struct module binary pattern characters.
Args:
description (
OrderedDict[
str, BasicParsingElement | list[BasicParsingElement | OrderedDict] | OrderedDict
]
): Type tree of BasicParsingElement nodes.
description (TypeTree): Type tree of BasicParsingElement nodes.
Returns:
str: Struct module pattern string.
Expand All @@ -157,11 +147,11 @@ def build_data_pattern(
elif isinstance(sub_element, OrderedDict):
pattern += build_data_pattern(sub_element)
else:
raise Exception(f"invalid list type found ({name})")
raise TypeError(f"invalid list type found ({name})")
elif isinstance(root_element, OrderedDict):
pattern += build_data_pattern(root_element)
else:
raise Exception(f"invalid element type found ({name}: {type(root_element)})")
raise TypeError(f"invalid element type found ({name}: {type(root_element)})")
return pattern


Expand All @@ -170,9 +160,6 @@ def resolve_basic_type(
) -> int | float | bytes:
"""Returns the value of the element while checking the intended type in the node.
Raises:
Exception: [description]
Returns:
int | float | bytes: Pythonic parsed value.
"""
Expand All @@ -181,25 +168,18 @@ def resolve_basic_type(
return arg
# if isinstance(arg, bytes) and element.python_type is str:
# return arg.decode("utf-8")
raise Exception(f"invalid match between types: {type(arg)} != {element.python_type}")
raise TypeError(f"invalid match between types: {type(arg)} != {element.python_type}")


def build_structure(
args: list[int | float | bytes],
description: OrderedDict[
str,
BasicParsingElement | type | list[BasicParsingElement | OrderedDict] | OrderedDict,
],
description: TypeTree,
) -> Any:
"""Constructs an instantiation of the data type described by the description argument.
Args:
args (list[int): Flat list of values returned from the struct module.
description (
OrderedDict[
str, BasicParsingElement | list[BasicParsingElement | OrderedDict] | OrderedDict
]
): Type tree of BasicParsingElement nodes.
description (TypeTree): Type tree of BasicParsingElement nodes.
Returns:
Any: Instantiated dataclass
Expand All @@ -208,9 +188,9 @@ def build_structure(
# print(f"dat_args: {args}")
cls_type = description.get("__struct_type__")
if cls_type is not None and not isinstance(cls_type, type):
raise Exception("lost struct type information in description")
raise TypeError("lost struct type information in description")
if cls_type is None:
raise Exception("unable to find type information in description")
raise LookupError("unable to find type information in description")
cls_args: dict[str, Any] = {}
# print(f"constructing type {cls_type}")

Expand All @@ -228,12 +208,12 @@ def build_structure(
elif isinstance(sub_element, OrderedDict):
list_element.append(build_structure(args, sub_element))
else:
raise Exception(f"invalid list type found ({name})")
raise TypeError(f"invalid list type found ({name})")
cls_args[name] = list_element
elif isinstance(root_element, OrderedDict):
cls_args[name] = build_structure(args, root_element)
else:
raise Exception(f"invalid element type found ({name}: {type(root_element)})")
raise TypeError(f"invalid element type found ({name}: {type(root_element)})")

# print(f"cls_args: {cls_args}")
return cls_type(**cls_args)
23 changes: 18 additions & 5 deletions bytechomp/datatypes/lookups.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,23 @@

from typing import NewType, Final

from bytechomp.datatypes.declarations import * # pylint: disable=wildcard-import
from bytechomp.datatypes.declarations import (
PAD,
U8,
U16,
U32,
U64,
I8,
I16,
I32,
I64,
F16,
F32,
F64,
)

ELEMENTARY_TYPE = type | NewType
ELEMENTARY_TYPE_LIST: Final[list[ELEMENTARY_TYPE]] = [ # type: ignore
ELEMENTARY_TYPE_LIST: Final[list[ELEMENTARY_TYPE]] = [
PAD,
U8,
U16,
Expand All @@ -24,7 +37,7 @@
float,
]

TYPE_TO_TAG: Final[dict[ELEMENTARY_TYPE, str]] = { # type: ignore
TYPE_TO_TAG: Final[dict[ELEMENTARY_TYPE, str]] = {
PAD: "x",
U8: "B",
U16: "H",
Expand All @@ -41,7 +54,7 @@
float: "d",
}

TYPE_TO_PYTYPE: Final[dict[ELEMENTARY_TYPE, type]] = { # type: ignore
TYPE_TO_PYTYPE: Final[dict[ELEMENTARY_TYPE, type]] = {
PAD: int,
U8: int,
U16: int,
Expand All @@ -58,7 +71,7 @@
float: float,
}

TYPE_TO_LENGTH: Final[dict[ELEMENTARY_TYPE, int]] = { # type: ignore
TYPE_TO_LENGTH: Final[dict[ELEMENTARY_TYPE, int]] = {
PAD: 1,
U8: 1,
U16: 2,
Expand Down
14 changes: 8 additions & 6 deletions bytechomp/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

from __future__ import annotations
from typing import Generic, TypeVar, Iterable, Iterator
from typing import Generic, TypeVar, Iterable, Iterator, cast
from dataclasses import is_dataclass
from collections import OrderedDict
from struct import Struct
Expand All @@ -14,6 +14,7 @@
build_data_description,
build_data_pattern,
build_structure,
TypeTree,
)

T = TypeVar("T") # pylint: disable=invalid-name
Expand All @@ -30,7 +31,7 @@ def __init__(self, byte_order: ByteOrder = ByteOrder.NATIVE) -> None:
self.__datatype: type | None = None
self.__byte_order = byte_order
self.__data: bytes = b""
self.__data_description: OrderedDict = OrderedDict()
self.__data_description: TypeTree = OrderedDict()
self.__data_pattern: str = ""
self.__struct = Struct(self.__data_pattern)

Expand Down Expand Up @@ -76,14 +77,14 @@ def feed(self, data: bytes) -> None:

self.__data += data

def __lshift__(self, data: bytes) -> Reader:
def __lshift__(self, data: bytes) -> Reader[T]:
"""Alternative to the feed method.
Args:
data (bytes): Binary data
Returns:
Reader: Binary protocol reader.
Reader[T]: Binary protocol reader.
"""

self.feed(data)
Expand Down Expand Up @@ -127,8 +128,9 @@ def build(self) -> T | None:
struct_bytes = self.__data[: self.__struct.size]
self.__data = self.__data[self.__struct.size :]
# print(f"unpacked: {self.__struct.unpack(struct_bytes)}")
return build_structure(
list(self.__struct.unpack(struct_bytes)), self.__data_description
return cast(
T,
build_structure(list(self.__struct.unpack(struct_bytes)), self.__data_description),
)
return None

Expand Down
Loading

0 comments on commit 300d089

Please sign in to comment.