From 8e1afc71b85f14f1be2977cf32e8966cc161393a Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Thu, 27 Feb 2020 07:40:18 +0100 Subject: [PATCH 01/18] Initial dataclass chekin. --- binmap/__init__.py | 83 ++++++- tests/test_binmap_dataclass.py | 426 +++++++++++++++++++++++++++++++++ 2 files changed, 502 insertions(+), 7 deletions(-) create mode 100644 tests/test_binmap_dataclass.py diff --git a/binmap/__init__.py b/binmap/__init__.py index 5c6f988..f7e5761 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -1,3 +1,4 @@ +import dataclasses import struct from inspect import Parameter, Signature @@ -7,7 +8,7 @@ class BaseDescriptor: :param name: Variable name""" - def __init__(self, name): + def __set_name__(self, obj, name): self.name = name @@ -26,6 +27,11 @@ def __set__(self, obj, value): class PaddingField(BaseDescriptor): """PaddingField descriptor is used to "pad" data with values unused for real data""" + def __init__(self): + self.metadata = {"format": "x"} + self.default = 0 + self.default_factory = dataclasses.MISSING + def __get__(self, obj, owner): """Getting values fails""" raise AttributeError(f"Padding ({self.name}) is not readable") @@ -60,7 +66,10 @@ class ConstField(BinField): """ConstField descriptor keeps it's value""" def __set__(self, obj, value): - raise AttributeError(f"{self.name} is a constant") + if self.name in obj.__dict__: + raise AttributeError(f"{self.name} is a constant") + else: + obj.__dict__[self.name] == value class BinmapMetaclass(type): @@ -97,14 +106,14 @@ def __new__(cls, clsname, bases, clsdict): setattr(clsobject, const.upper(), value) for name in keys: if name.startswith("_pad"): - setattr(clsobject, name, PaddingField(name=name)) + setattr(clsobject, name, PaddingField()) elif name in clsobject._constants: - setattr(clsobject, name, ConstField(name=name)) + setattr(clsobject, name, ConstField()) elif name in clsobject._enums: - setattr(clsobject, name, EnumField(name=name)) - setattr(clsobject, f"_{name}", BinField(name=f"_{name}")) + setattr(clsobject, name, EnumField()) + # setattr(clsobject, f"_{name}", BinField(name=f"_{name}")) else: - setattr(clsobject, name, BinField(name=name)) + setattr(clsobject, name, BinField()) return clsobject @@ -223,3 +232,63 @@ def __str__(self): if not key.startswith("_pad"): retval += ", %s=%s" % (key, getattr(self, key)) return retval + + +@dataclasses.dataclass +class BinmapDataclass: + _byteorder = "" + __binarydata: dataclasses.InitVar[int] = b"" + + def __init_subclass__(cls, byteorder=">"): + cls._byteorder = byteorder + + def __bytes__(self): + return struct.pack( + self._formatstring, + *( + v + for k, v in self.__dict__.items() + if k not in ["_byteorder", "_formatstring", "_binarydata"] + ), + # *(v for k, v in self.__dict__.items()), + ) + + def __post_init__(self, _binarydata): + formatstring = self._byteorder + for variable in self.__dict__.keys(): + if "format" in self.__dataclass_fields__[variable].metadata: + formatstring += self.__dataclass_fields__[variable].metadata["format"] + setattr(self, "_formatstring", formatstring) + if _binarydata != b"": + self._unpacker(_binarydata) + + def _unpacker(self, value): + args = struct.unpack(self._formatstring, value) + datafields = [f for f in self.__dict__ if not f.startswith("_")] + for arg, name in zip(args, datafields): + setattr(self, name, arg) + + def frombytes(self, value): + self._unpacker(value) + self._binarydata = value + + +def field(formatstring, default=None): + if default: + return dataclasses.field(default=default) + if formatstring in "BbHhIiLlQq": + default = 0 + elif formatstring in "efd": + default = 0.0 + elif formatstring == "c": + default = b"\x00" + else: + default = "" + return dataclasses.field(default=default, metadata={"format": formatstring}) +def enum(formatstring, enums): + return dataclasses.field(default=None) + + +def const(formatstring, default): + value = ConstField() + return dataclasses.field(default=value, metadata={"format": formatstring},) diff --git a/tests/test_binmap_dataclass.py b/tests/test_binmap_dataclass.py new file mode 100644 index 0000000..ccf4b89 --- /dev/null +++ b/tests/test_binmap_dataclass.py @@ -0,0 +1,426 @@ +from dataclasses import dataclass +import struct + +import pytest + +import binmap + + +def test_baseclass(): + b = binmap.BinmapDataclass() + assert type(b) == binmap.BinmapDataclass + assert str(b) == "BinmapDataclass()" + + +def test_baseclass_with_keyword(): + with pytest.raises(TypeError) as excinfo: + binmap.BinmapDataclass(temp=10) + assert "got an unexpected keyword argument 'temp'" in str(excinfo) + + +@dataclass +class Temp(binmap.BinmapDataclass): + temp: int = binmap.field("B") + + +@dataclass +class TempHum(binmap.BinmapDataclass): + temp: int = binmap.field("B") + humidity: int = binmap.field("B") + + +def test_different_classes_eq(): + t = Temp(temp=10) + th = TempHum(temp=10, humidity=60) + assert t != th + assert t.temp == th.temp + + +@dataclass +class Bigendian(binmap.BinmapDataclass): + value: int = binmap.field("q") + + +@dataclass +class Littleedian(binmap.BinmapDataclass, byteorder="<"): + value: int = binmap.field("q") + + +def test_dataformats(): + be = Bigendian(value=-10) + le = Littleedian(value=-10) + + assert be.value == le.value + assert bytes(be) == b"\xff\xff\xff\xff\xff\xff\xff\xf6" + assert bytes(le) == b"\xf6\xff\xff\xff\xff\xff\xff\xff" + + assert bytes(be) != bytes(le) + + be.frombytes(b"\xff\xff\xff\xff\xf4\xff\xff\xf6") + le.frombytes(b"\xff\xff\xff\xff\xf4\xff\xff\xf6") + + assert be.value == -184549386 + assert le.value == -648518393585991681 + + assert be.value != le.value + assert bytes(be) == bytes(le) + + +class TestTempClass: + def test_with_argument(self): + t = Temp(temp=10) + assert t.temp == 10 + assert bytes(t) == b"\x0a" + assert str(t) == "Temp(temp=10)" + + def test_without_argument(self): + t = Temp() + assert t.temp == 0 + assert bytes(t) == b"\x00" + + def test_unknown_argument(self): + with pytest.raises(TypeError) as excinfo: + Temp(hum=60) + assert "got an unexpected keyword argument 'hum'" in str(excinfo) + + def test_value(self): + t = Temp() + t.temp = 10 + assert bytes(t) == b"\x0a" + + def test_raw(self): + t = Temp(b"\x0a") + assert t.temp == 10 + + def test_update_binarydata(self): + t = Temp(b"\x0a") + assert t.temp == 10 + t.frombytes(b"\x14") + assert t.temp == 20 + + def test_change_value(self): + t = Temp(temp=10) + assert bytes(t) == b"\x0a" + + t.temp = 20 + assert bytes(t) == b"\x14" + + def test_value_bounds(self): + t = Temp() + with pytest.raises(struct.error) as excinfo: + t.temp = 256 + assert "ubyte format requires 0 <= number <= 255" in str(excinfo) + + with pytest.raises(struct.error) as excinfo: + t.temp = -1 + assert "ubyte format requires 0 <= number <= 255" in str(excinfo) + + def test_compare_equal(self): + t1 = Temp(temp=10) + t2 = Temp(temp=10) + assert t1.temp == t2.temp + assert t1 == t2 + + def test_compare_not_equal(self): + t1 = Temp(temp=10) + t2 = Temp(temp=20) + assert t1.temp != t2.temp + assert t1 != t2 + + +class TestTempHumClass: + def test_with_argument(self): + th = TempHum(temp=10, humidity=60) + assert th.temp == 10 + assert th.humidity == 60 + assert str(th) == "TempHum(temp=10, humidity=60)" + + def test_without_argument(self): + th = TempHum() + assert th.temp == 0 + assert th.humidity == 0 + assert bytes(th) == b"\x00\x00" + + def test_raw(self): + th = TempHum(b"\x0a\x46") + assert th.temp == 10 + assert th.humidity == 70 + + def test_change_values(self): + th = TempHum(temp=10, humidity=70) + th.temp = 30 + th.humidity = 30 + assert th.temp == 30 + assert th.humidity == 30 + assert bytes(th) == b"\x1e\x1e" + + def test_compare_equal(self): + th1 = TempHum(temp=10, humidity=70) + th2 = TempHum(temp=10, humidity=70) + assert th1.temp == th2.temp + assert th1 == th2 + + def test_compare_not_equal(self): + th1 = TempHum(temp=10, humidity=70) + th2 = TempHum(temp=20, humidity=60) + th3 = TempHum(temp=10, humidity=60) + th4 = TempHum(temp=20, humidity=70) + assert (th1.temp != th2.temp) and (th1.humidity != th2.humidity) + assert th1 != th2 + assert th1 != th3 + assert th1 != th4 + assert th2 != th3 + assert th2 != th4 + + +@dataclass +class Pad(binmap.BinmapDataclass): + temp: int = binmap.field("B") + pad: any = binmap.padding(2) + humidity: int = binmap.field("B") + + +@dataclass +class AdvancedPad(binmap.BinmapDataclass): + temp: int = binmap.field("B") + _pad1: any = binmap.padding(2) + humidity: int = binmap.field("B") + _pad2: any = binmap.padding(3) + _pad3: any = binmap.padding(1) + + +class TestPadClass: + def test_create_pad(self): + p = Pad(temp=10, humidity=60) + with pytest.raises(AttributeError) as excinfo: + p.pad + assert "Padding (pad) is not readable" in str(excinfo) + assert p.temp == 10 + assert p.humidity == 60 + assert str(p) == "Pad(temp=10, humidity=60)" + + def test_parse_data(self): + p = Pad(b"\x0a\x10\x20\x3c") + with pytest.raises(AttributeError) as excinfo: + p._pad + assert p.temp == 10 + assert "Padding (_pad) is not readable" in str(excinfo) + assert p.humidity == 60 + + def test_pack_data(self): + p = Pad() + p.temp = 10 + p.humidity = 60 + assert bytes(p) == b"\x0a\x00\x00\x3c" + + def test_advanced_pad(self): + p = AdvancedPad(temp=10, humidity=60) + with pytest.raises(AttributeError) as excinfo: + p._pad1 + assert "Padding (_pad1) is not readable" in str(excinfo) + with pytest.raises(AttributeError) as excinfo: + p._pad2 + assert "Padding (_pad1) is not readable" in str(excinfo) + with pytest.raises(AttributeError) as excinfo: + p._pad3 + assert "Padding (_pad1) is not readable" in str(excinfo) + assert p.temp == 10 + assert p.humidity == 60 + + def test_advanced_parse_data(self): + p = AdvancedPad(b"\n\x00\x00<\x00\x00\x00\x00") + with pytest.raises(AttributeError) as excinfo: + p._pad1 + assert "Padding (_pad1) is not readable" in str(excinfo) + with pytest.raises(AttributeError) as excinfo: + p._pad2 + assert "Padding (_pad2) is not readable" in str(excinfo) + assert p.humidity == 60 + assert p.temp == 10 + assert str(p) == "AdvancedPad(temp=10, humidity=60)" + + def test_advanced_pack_data(self): + p = AdvancedPad() + p.temp = 10 + p.humidity = 60 + assert bytes(p) == b"\n\x00\x00<\x00\x00\x00\x00" + + +@dataclass +class EnumClass(binmap.BinmapDataclass): + temp: int = binmap.field("B") + wind: int = binmap.enum("h", {0: "North", 1: "East", 2: "South", 4: "West"}) + + +@pytest.mark.skip(reason="Enums need to be redesigned") +class TestEnumClass: + def test_create_class(self): + ec = EnumClass() + assert ec + assert EnumClass.SOUTH == 2 + + def test_get_enum(self): + ec = EnumClass(temp=10, wind=2) + assert ec.wind == "South" + assert str(ec) == "EnumClass, temp=10, wind=South" + + def test_enum_binary(self): + ec = EnumClass(b"\x0a\x02") + assert ec.wind == "South" + assert str(ec) == "EnumClass, temp=10, wind=South" + + def test_set_named_enum(self): + ec = EnumClass() + ec.wind = "South" + assert bytes(ec) == b"\x00\x02" + + with pytest.raises(ValueError) as excinfo: + ec.wind = "Norhtwest" + assert "Unknown enum or value" in str(excinfo) + + with pytest.raises(ValueError) as excinfo: + ec.wind = 1.2 + assert "Unknown enum or value" in str(excinfo) + + def test_colliding_enums(self): + with pytest.raises(ValueError) as excinfo: + + class EnumCollide(binmap.BinmapDataclass): + _datafields = { + "wind1": "B", + "wind2": "B", + } + _enums = { + "wind1": {0: "North"}, + "wind2": {2: "North"}, + } + + assert "North already defined" in str(excinfo) + + +@dataclass +class ConstValues(binmap.BinmapDataclass): + datatype: int = binmap.const("B", 0x15) + status: int = binmap.field("B") + + +class TestConstValues: + def test_create_class(self): + c = ConstValues() + with pytest.raises(AttributeError) as excinfo: + ConstValues(datatype=0x14, status=1) + assert "datatype is a constant" in str(excinfo) + assert c.datatype == 0x15 + + def test_set_value(self): + c = ConstValues(status=1) + with pytest.raises(AttributeError) as excinfo: + c.datatype = 0x14 + assert "datatype is a constant" in str(excinfo) + assert c.datatype == 0x15 + assert c.status == 1 + assert bytes(c) == b"\x15\x01" + + def test_binary_data(self): + c = ConstValues(b"\x15\x01") + with pytest.raises(ValueError) as excinfo: + ConstValues(b"\x14\x01") + assert "Constant doesn't match binary data" in str(excinfo) + assert c.datatype == 0x15 + assert c.status == 1 + + +@dataclass +class AllDatatypes(binmap.BinmapDataclass): + _pad: any = binmap.padding(1) + char: int = binmap.field("c") + signedchar: int = binmap.field("b") + unsignedchar: int = binmap.field("B") + boolean: bool = binmap.field("?") + short: int = binmap.field("h") + unsignedshort: int = binmap.field("H") + integer: int = binmap.field("i") + unsignedint: int = binmap.field("I") + long: int = binmap.field("l") + unsignedlong: int = binmap.field("L") + longlong: int = binmap.field("q") + unsignedlonglong: int = binmap.field("Q") + halffloat: float = binmap.field("e") + floating: float = binmap.field("f") + double: float = binmap.field("d") + string: bytes = binmap.field("10s") + pascalstring: bytes = binmap.field("15p") + + +class TestAllDatatypes: + def test_create_class(self): + sc = AllDatatypes() + assert sc + + def test_with_arguments(self): + sc = AllDatatypes( + char=b"%", + signedchar=-2, + unsignedchar=5, + boolean=True, + short=-7, + unsignedshort=17, + integer=-15, + unsignedint=11, + long=-2312, + unsignedlong=2212, + longlong=-1212, + unsignedlonglong=4444, + halffloat=3.5, + floating=3e3, + double=13e23, + string=b"helloworld", + pascalstring=b"hello pascal", + ) + assert sc.char == b"%" + assert sc.signedchar == -2 + assert sc.unsignedchar == 5 + assert sc.boolean + assert sc.short == -7 + assert sc.unsignedshort == 17 + assert sc.integer == -15 + assert sc.unsignedint == 11 + assert sc.long == -2312 + assert sc.unsignedlong == 2212 + assert sc.longlong == -1212 + assert sc.unsignedlonglong == 4444 + assert sc.halffloat == 3.5 + assert sc.floating == 3e3 + assert sc.double == 13e23 + assert sc.string == b"helloworld" + assert sc.pascalstring == b"hello pascal" + assert ( + bytes(sc) + == b"\x00%\xfe\x05\x01\xff\xf9\x00\x11\xff\xff\xff\xf1\x00\x00\x00\x0b\xff\xff\xf6\xf8\x00\x00" + b"\x08\xa4\xff\xff\xff\xff\xff\xff\xfbD\x00\x00\x00\x00\x00\x00\x11\\C\x00E;\x80\x00D\xf14\x92Bg\x0c" + b"\xe8helloworld\x0chello pascal\x00\x00" + ) + + def test_with_binarydata(self): + sc = AllDatatypes( + b"\x00W\xee\x15\x00\xf4\xf9\x10\x11\xff\xff\xff1\x00\x00\x01\x0b\xff\xff\xe6\xf8\x00\x00\x18" + b"\xa4\xff\xff\xff\xff\xff\xff\xfbE\x00\x00\x00\x00\x00\x01\x11\\C\x01E;\x81\x00D\xf14\xa2Bg\x0c" + b"\xe8hi world \x09hi pascal\x00\x00\x00\x00\x00" + ) + assert sc.char == b"W" + assert sc.signedchar == -18 + assert sc.unsignedchar == 21 + assert not sc.boolean + assert sc.short == -2823 + assert sc.unsignedshort == 4113 + assert sc.integer == -207 + assert sc.unsignedint == 267 + assert sc.long == -6408 + assert sc.unsignedlong == 6308 + assert sc.longlong == -1211 + assert sc.unsignedlonglong == 69980 + assert sc.halffloat == 3.501953125 + assert sc.floating == 3000.0625 + assert sc.double == 1.3000184467440736e24 + assert sc.string == b"hi world " + assert sc.pascalstring == b"hi pascal" From 77fced932faaf6b4d78c4763ec05d1a1e7e3fc49 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sun, 1 Mar 2020 14:41:07 +0100 Subject: [PATCH 02/18] First somewhat working version of dataclass. --- binmap/__init__.py | 251 ++++--------------- tests/test_binmap.py | 348 +++++++++++++-------------- tests/test_binmap_dataclass.py | 426 --------------------------------- 3 files changed, 220 insertions(+), 805 deletions(-) delete mode 100644 tests/test_binmap_dataclass.py diff --git a/binmap/__init__.py b/binmap/__init__.py index f7e5761..638de91 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -1,6 +1,9 @@ import dataclasses import struct -from inspect import Parameter, Signature +from abc import ABC +from typing import Dict, NewType, Tuple, Type, TypeVar, get_type_hints + +T = TypeVar("T") class BaseDescriptor: @@ -11,6 +14,9 @@ class BaseDescriptor: def __set_name__(self, obj, name): self.name = name + def __init__(self, name=""): + self.name = name + class BinField(BaseDescriptor): """BinField descriptor tries to pack it into a struct before setting the @@ -20,18 +26,15 @@ def __get__(self, obj, owner): return obj.__dict__[self.name] def __set__(self, obj, value): - struct.pack(obj._datafields[self.name], value) + type_hints = get_type_hints(obj) + struct.pack(datatypemapping[type_hints[self.name]][1], value) + # struct.pack(obj._datafields[self.name], value) obj.__dict__[self.name] = value class PaddingField(BaseDescriptor): """PaddingField descriptor is used to "pad" data with values unused for real data""" - def __init__(self): - self.metadata = {"format": "x"} - self.default = 0 - self.default_factory = dataclasses.MISSING - def __get__(self, obj, owner): """Getting values fails""" raise AttributeError(f"Padding ({self.name}) is not readable") @@ -69,226 +72,78 @@ def __set__(self, obj, value): if self.name in obj.__dict__: raise AttributeError(f"{self.name} is a constant") else: - obj.__dict__[self.name] == value - - -class BinmapMetaclass(type): - """Metaclass for :class:`Binmap` and all subclasses of :class:`Binmap`. - - :class:`BinmapMetaclass` responsibility is to add for adding variables from - _datafields, _enums, _constants and add keyword only parameters. - - _datafields starting with ``_pad`` does't get any instance variable mapped. - - _enums get a variable called _{name} which has the binary data.""" - - def __new__(cls, clsname, bases, clsdict): - clsobject = super().__new__(cls, clsname, bases, clsdict) - keys = clsobject._datafields.keys() - sig = Signature( - [ - Parameter( - "binarydata", - Parameter.POSITIONAL_OR_KEYWORD, - default=Parameter.default, - ) - ] - + [ - Parameter(name, Parameter.KEYWORD_ONLY, default=Parameter.default) - for name in keys - ] - ) - setattr(clsobject, "__signature__", sig) - for enum in clsobject._enums: - for value, const in clsobject._enums[enum].items(): - if hasattr(clsobject, const.upper()): - raise ValueError(f"{const} already defined") - setattr(clsobject, const.upper(), value) - for name in keys: - if name.startswith("_pad"): - setattr(clsobject, name, PaddingField()) - elif name in clsobject._constants: - setattr(clsobject, name, ConstField()) - elif name in clsobject._enums: - setattr(clsobject, name, EnumField()) - # setattr(clsobject, f"_{name}", BinField(name=f"_{name}")) - else: - setattr(clsobject, name, BinField()) - return clsobject - - -class Binmap(metaclass=BinmapMetaclass): - """A class that maps to and from binary data using :py:class:`struct`. - All fields in :attr:`binmap.Binmap._datafields` gets a corresponding - variable in instance, except variables starting with ``_pad``, which is - just padding. - - To create an enum mapping you could add :attr:`binmap.Binmap._enums` with a - map to your corresponing datafield: - - .. code-block:: python - - class TempWind(Binmap): - _datafields = {"temp": "b", "wind": "B"} - _enums = {"wind": {0: "North", 1: "East", 2: "South", 4: "West"}} - - tw = TempWind() - tw.temp = 3 - tw.wind = "South" - print(bytes(tw)) - b'\\x03\\x02' - - - """ - - #: _byteorder: charcter with byteorder - _byteorder = ">" - #: _datafields: dict with variable name as key and :py:class:`struct` `format strings`_ as value - _datafields = {} - #: _enums: dict of dicts containing maps of strings - _enums = {} - #: _constants: dict of constants. This creates a variable that is allways - #: the same value. It won't accept binary data with any other value - _constants = {} - - def __init__(self, *args, **kwargs): - self._formatstring = self._byteorder - for fmt in self._datafields.values(): - self._formatstring += fmt - - bound = self.__signature__.bind(*args, **kwargs) - - if "binarydata" in bound.arguments: - self._binarydata = bound.arguments["binarydata"] - self._unpacker(self._binarydata) - - for param in self.__signature__.parameters.values(): - if param.name == "binarydata": - continue - if param.name in bound.arguments: - if param.name in self._constants: - raise AttributeError(f"{param.name} is a constant") - setattr(self, param.name, bound.arguments[param.name]) - elif param.name in self._constants: - self.__dict__[param.name] = self._constants[param.name] - elif param.name not in self.__dict__: - if self._datafields[param.name] in "BbHhIiLlQq": - setattr(self, param.name, 0) - elif self._datafields[param.name] in "efd": - setattr(self, param.name, 0.0) - elif self._datafields[param.name] == "c": - setattr(self, param.name, b"\x00") - else: - setattr(self, param.name, b"") - - if len(args) == 1: - self._binarydata = args[0] - self._unpacker(self._binarydata) - else: - self._binarydata = b"" + obj.__dict__.update({self.name: value}) - def _unpacker(self, value): - args = struct.unpack(self._formatstring, value) - datafields = [ - field for field in self._datafields if not field.startswith("_pad") - ] - for arg, name in zip(args, datafields): - if name in self._constants: - if arg != self._constants[name]: - raise ValueError("Constant doesn't match binary data") - else: - setattr(self, name, arg) - def __bytes__(self): - """packs or unpacks all variables to a binary structure defined by - _datafields' format values""" - datas = [] - for var in self._datafields: - if not var.startswith("_pad"): - if var in self._enums: - datas.append(getattr(self, f"_{var}")) - else: - datas.append(getattr(self, var)) - return struct.pack(self._formatstring, *datas) +unsignedchar = NewType("unsingedchar", int) +longlong = NewType("longlong", int) +padding = NewType("padding", int) +constant = Tuple + +datatypemapping: Dict[type, Tuple[BaseDescriptor, str]] = { + unsignedchar: (BinField, "B"), + longlong: (BinField, "q"), + padding: (PaddingField, "x"), + constant: (ConstField, "B"), +} - def frombytes(self, value): - self._unpacker(value) - self._binarydata = value - def __eq__(self, other): - if self.__signature__ != other.__signature__: - return False - for field in self._datafields: - v1 = getattr(self, field) - v2 = getattr(other, field) - if v1 != v2: - return False - return True - - def __str__(self): - retval = f"{self.__class__.__name__}" - if self._datafields: - for key in self._datafields: - if not key.startswith("_pad"): - retval += ", %s=%s" % (key, getattr(self, key)) - return retval +def binmapdataclass(cls: Type[T]) -> Type[T]: + + type_hints = get_type_hints(cls) + + cls._formatstring = "" + # Used to list all fields and locate fields by field number. + for field_ in dataclasses.fields(cls): + if getattr(field_.type, "__origin__", None) is tuple: + _, _type = datatypemapping[field_.type.__args__[0]] + _base = ConstField + else: + _base, _type = datatypemapping[type_hints[field_.name]] + setattr(cls, field_.name, _base(name=field_.name)) + if type_hints[field_.name] is padding: + _type = field_.default * _type + cls._formatstring += _type + + return cls @dataclasses.dataclass -class BinmapDataclass: +class BinmapDataclass(ABC): _byteorder = "" - __binarydata: dataclasses.InitVar[int] = b"" + _formatstring = "" + __binarydata: dataclasses.InitVar[bytes] = b"" def __init_subclass__(cls, byteorder=">"): cls._byteorder = byteorder def __bytes__(self): return struct.pack( - self._formatstring, + # TODO: use datclass.fields + self._byteorder + self._formatstring, *( v for k, v in self.__dict__.items() if k not in ["_byteorder", "_formatstring", "_binarydata"] ), - # *(v for k, v in self.__dict__.items()), ) def __post_init__(self, _binarydata): - formatstring = self._byteorder - for variable in self.__dict__.keys(): - if "format" in self.__dataclass_fields__[variable].metadata: - formatstring += self.__dataclass_fields__[variable].metadata["format"] - setattr(self, "_formatstring", formatstring) if _binarydata != b"": self._unpacker(_binarydata) def _unpacker(self, value): - args = struct.unpack(self._formatstring, value) - datafields = [f for f in self.__dict__ if not f.startswith("_")] + type_hints = get_type_hints(self) + datafields = [ + f.name + for f in dataclasses.fields(self) + if not (type_hints[f.name] is padding) + and not (getattr(f.type, "__origin__", None)) + ] + args = struct.unpack(self._byteorder + self._formatstring, value) for arg, name in zip(args, datafields): setattr(self, name, arg) def frombytes(self, value): self._unpacker(value) self._binarydata = value - - -def field(formatstring, default=None): - if default: - return dataclasses.field(default=default) - if formatstring in "BbHhIiLlQq": - default = 0 - elif formatstring in "efd": - default = 0.0 - elif formatstring == "c": - default = b"\x00" - else: - default = "" - return dataclasses.field(default=default, metadata={"format": formatstring}) -def enum(formatstring, enums): - return dataclasses.field(default=None) - - -def const(formatstring, default): - value = ConstField() - return dataclasses.field(default=value, metadata={"format": formatstring},) diff --git a/tests/test_binmap.py b/tests/test_binmap.py index 1c1d27b..fd5f0e7 100644 --- a/tests/test_binmap.py +++ b/tests/test_binmap.py @@ -1,4 +1,5 @@ import struct +from dataclasses import dataclass import pytest @@ -6,51 +7,28 @@ def test_baseclass(): - b = binmap.Binmap() - assert type(b) == binmap.Binmap - assert str(b) == "Binmap" + b = binmap.BinmapDataclass() + assert type(b) == binmap.BinmapDataclass + assert str(b) == "BinmapDataclass()" def test_baseclass_with_keyword(): with pytest.raises(TypeError) as excinfo: - binmap.Binmap(temp=10) + binmap.BinmapDataclass(temp=10) assert "got an unexpected keyword argument 'temp'" in str(excinfo) -def test_illegal_fieldnames(): - with pytest.raises(ValueError) as excinfo: +@binmap.binmapdataclass +@dataclass +class Temp(binmap.BinmapDataclass): + temp: binmap.unsignedchar = 0 - class Space(binmap.Binmap): - _datafields = { - " ": "B", - } - assert "' ' is not a valid parameter name" in str(excinfo) - - with pytest.raises(ValueError) as excinfo: - - class BadName(binmap.Binmap): - _datafields = { - "-a": "B", - } - - assert "'-a' is not a valid parameter name" in str(excinfo) - with pytest.raises(ValueError) as excinfo: - - class Number(binmap.Binmap): - _datafields = { - "1": "B", - } - - assert "'1' is not a valid parameter name" in str(excinfo) - - -class Temp(binmap.Binmap): - _datafields = {"temp": "B"} - - -class TempHum(binmap.Binmap): - _datafields = {"temp": "B", "humidity": "B"} +@binmap.binmapdataclass +@dataclass +class TempHum(binmap.BinmapDataclass): + temp: binmap.unsignedchar = 0 + humidity: binmap.unsignedchar = 0 def test_different_classes_eq(): @@ -60,17 +38,19 @@ def test_different_classes_eq(): assert t.temp == th.temp -class Bigendian(binmap.Binmap): - _datafields = {"value": "q"} +@binmap.binmapdataclass +@dataclass +class Bigendian(binmap.BinmapDataclass): + value: binmap.longlong = 0 -class Littleedian(binmap.Binmap): - _byteorder = "<" - _datafields = {"value": "q"} +@binmap.binmapdataclass +@dataclass +class Littleedian(binmap.BinmapDataclass, byteorder="<"): + value: binmap.longlong = 0 def test_dataformats(): - be = Bigendian(value=-10) le = Littleedian(value=-10) @@ -94,7 +74,8 @@ class TestTempClass: def test_with_argument(self): t = Temp(temp=10) assert t.temp == 10 - assert str(t) == "Temp, temp=10" + assert bytes(t) == b"\x0a" + assert str(t) == "Temp(temp=10)" def test_without_argument(self): t = Temp() @@ -156,7 +137,7 @@ def test_with_argument(self): th = TempHum(temp=10, humidity=60) assert th.temp == 10 assert th.humidity == 60 - assert str(th) == "TempHum, temp=10, humidity=60" + assert str(th) == "TempHum(temp=10, humidity=60)" def test_without_argument(self): th = TempHum() @@ -196,36 +177,40 @@ def test_compare_not_equal(self): assert th2 != th4 -class Pad(binmap.Binmap): - _datafields = {"temp": "B", "_pad1": "xx", "humidity": "B"} +@binmap.binmapdataclass +@dataclass +class Pad(binmap.BinmapDataclass): + temp: binmap.unsignedchar = 0 + pad: binmap.padding = 2 + humidity: binmap.unsignedchar = 0 -class AdvancedPad(binmap.Binmap): - _datafields = { - "temp": "B", - "_pad1": "xx", - "humidity": "B", - "_pad2": "3x", - "_pad3": "x", - } +@binmap.binmapdataclass +@dataclass +class AdvancedPad(binmap.BinmapDataclass): + temp: binmap.unsignedchar = 0 + _pad1: binmap.padding = 2 + humidity: binmap.unsignedchar = 0 + _pad2: binmap.padding = 3 + _pad3: binmap.padding = 1 class TestPadClass: def test_create_pad(self): p = Pad(temp=10, humidity=60) with pytest.raises(AttributeError) as excinfo: - p._pad1 - assert "Padding (_pad1) is not readable" in str(excinfo) + p.pad + assert "Padding (pad) is not readable" in str(excinfo) assert p.temp == 10 assert p.humidity == 60 - assert str(p) == "Pad, temp=10, humidity=60" + assert str(p) == "Pad(temp=10, humidity=60)" def test_parse_data(self): p = Pad(b"\x0a\x10\x20\x3c") with pytest.raises(AttributeError) as excinfo: - p._pad1 + p.pad + assert "Padding (pad) is not readable" in str(excinfo) assert p.temp == 10 - assert "Padding (_pad1) is not readable" in str(excinfo) assert p.humidity == 60 def test_pack_data(self): @@ -241,10 +226,10 @@ def test_advanced_pad(self): assert "Padding (_pad1) is not readable" in str(excinfo) with pytest.raises(AttributeError) as excinfo: p._pad2 - assert "Padding (_pad2) is not readable" in str(excinfo) + assert "Padding (_pad1) is not readable" in str(excinfo) with pytest.raises(AttributeError) as excinfo: p._pad3 - assert "Padding (_pad3) is not readable" in str(excinfo) + assert "Padding (_pad1) is not readable" in str(excinfo) assert p.temp == 10 assert p.humidity == 60 @@ -258,7 +243,7 @@ def test_advanced_parse_data(self): assert "Padding (_pad2) is not readable" in str(excinfo) assert p.humidity == 60 assert p.temp == 10 - assert str(p) == "AdvancedPad, temp=10, humidity=60" + assert str(p) == "AdvancedPad(temp=10, humidity=60)" def test_advanced_pack_data(self): p = AdvancedPad() @@ -267,48 +252,47 @@ def test_advanced_pack_data(self): assert bytes(p) == b"\n\x00\x00<\x00\x00\x00\x00" -class EnumClass(binmap.Binmap): - _datafields = { - "temp": "B", - "wind": "B", - } - - _enums = {"wind": {0: "North", 1: "East", 2: "South", 4: "West"}} +@binmap.binmapdataclass +@dataclass +class EnumClass(binmap.BinmapDataclass): + temp: binmap.unsignedchar = 0 + # wind: int = binmap.enum("h", {0: "North", 1: "East", 2: "South", 4: "West"}) +@pytest.mark.skip(reason="Enums need to be redesigned") class TestEnumClass: def test_create_class(self): - pc = EnumClass() - assert pc + ec = EnumClass() + assert ec assert EnumClass.SOUTH == 2 def test_get_enum(self): - pc = EnumClass(temp=10, wind=2) - assert pc.wind == "South" - assert str(pc) == "EnumClass, temp=10, wind=South" + ec = EnumClass(temp=10, wind=2) + assert ec.wind == "South" + assert str(ec) == "EnumClass, temp=10, wind=South" def test_enum_binary(self): - pc = EnumClass(b"\x0a\x02") - assert pc.wind == "South" - assert str(pc) == "EnumClass, temp=10, wind=South" + ec = EnumClass(b"\x0a\x02") + assert ec.wind == "South" + assert str(ec) == "EnumClass, temp=10, wind=South" def test_set_named_enum(self): - pc = EnumClass() - pc.wind = "South" - assert bytes(pc) == b"\x00\x02" + ec = EnumClass() + ec.wind = "South" + assert bytes(ec) == b"\x00\x02" with pytest.raises(ValueError) as excinfo: - pc.wind = "Norhtwest" + ec.wind = "Norhtwest" assert "Unknown enum or value" in str(excinfo) with pytest.raises(ValueError) as excinfo: - pc.wind = 1.2 + ec.wind = 1.2 assert "Unknown enum or value" in str(excinfo) def test_colliding_enums(self): with pytest.raises(ValueError) as excinfo: - class EnumCollide(binmap.Binmap): + class EnumCollide(binmap.BinmapDataclass): _datafields = { "wind1": "B", "wind2": "B", @@ -321,9 +305,11 @@ class EnumCollide(binmap.Binmap): assert "North already defined" in str(excinfo) -class ConstValues(binmap.Binmap): - _datafields = {"datatype": "B", "status": "B"} - _constants = {"datatype": 0x15} +@binmap.binmapdataclass +@dataclass +class ConstValues(binmap.BinmapDataclass): + datatype: binmap.constant[binmap.unsignedchar] = 0x15 + status: binmap.unsignedchar = 0 class TestConstValues: @@ -352,98 +338,98 @@ def test_binary_data(self): assert c.status == 1 -class AllDatatypes(binmap.Binmap): - _datafields = { - "_pad": "x", - "char": "c", - "signedchar": "b", - "unsignedchar": "B", - "boolean": "?", - "short": "h", - "unsignedshort": "H", - "integer": "i", - "unsignedint": "I", - "long": "l", - "unsignedlong": "L", - "longlong": "q", - "unsignedlonglong": "Q", - "halffloat": "e", - "floating": "f", - "double": "d", - "string": "10s", - "pascalstring": "15p", - } - - -class TestAllDatatypes: - def test_create_class(self): - sc = AllDatatypes() - assert sc - - def test_with_arguments(self): - sc = AllDatatypes( - char=b"%", - signedchar=-2, - unsignedchar=5, - boolean=True, - short=-7, - unsignedshort=17, - integer=-15, - unsignedint=11, - long=-2312, - unsignedlong=2212, - longlong=-1212, - unsignedlonglong=4444, - halffloat=3.5, - floating=3e3, - double=13e23, - string=b"helloworld", - pascalstring=b"hello pascal", - ) - assert sc.char == b"%" - assert sc.signedchar == -2 - assert sc.unsignedchar == 5 - assert sc.boolean - assert sc.short == -7 - assert sc.unsignedshort == 17 - assert sc.integer == -15 - assert sc.unsignedint == 11 - assert sc.long == -2312 - assert sc.unsignedlong == 2212 - assert sc.longlong == -1212 - assert sc.unsignedlonglong == 4444 - assert sc.halffloat == 3.5 - assert sc.floating == 3e3 - assert sc.double == 13e23 - assert sc.string == b"helloworld" - assert sc.pascalstring == b"hello pascal" - assert ( - bytes(sc) - == b"\x00%\xfe\x05\x01\xff\xf9\x00\x11\xff\xff\xff\xf1\x00\x00\x00\x0b\xff\xff\xf6\xf8\x00\x00" - b"\x08\xa4\xff\xff\xff\xff\xff\xff\xfbD\x00\x00\x00\x00\x00\x00\x11\\C\x00E;\x80\x00D\xf14\x92Bg\x0c" - b"\xe8helloworld\x0chello pascal\x00\x00" - ) - - def test_with_binarydata(self): - sc = AllDatatypes( - b"\x00W\xee\x15\x00\xf4\xf9\x10\x11\xff\xff\xff1\x00\x00\x01\x0b\xff\xff\xe6\xf8\x00\x00\x18" - b"\xa4\xff\xff\xff\xff\xff\xff\xfbE\x00\x00\x00\x00\x00\x01\x11\\C\x01E;\x81\x00D\xf14\xa2Bg\x0c" - b"\xe8hi world \x09hi pascal\x00\x00\x00\x00\x00" - ) - assert sc.char == b"W" - assert sc.signedchar == -18 - assert sc.unsignedchar == 21 - assert not sc.boolean - assert sc.short == -2823 - assert sc.unsignedshort == 4113 - assert sc.integer == -207 - assert sc.unsignedint == 267 - assert sc.long == -6408 - assert sc.unsignedlong == 6308 - assert sc.longlong == -1211 - assert sc.unsignedlonglong == 69980 - assert sc.halffloat == 3.501953125 - assert sc.floating == 3000.0625 - assert sc.double == 1.3000184467440736e24 - assert sc.string == b"hi world " - assert sc.pascalstring == b"hi pascal" +# @binmap.binmapdataclass +# @dataclass +# class AllDatatypes(binmap.BinmapDataclass): +# _pad: any = binmap.padding(1) +# char: int = binmap.field("c") +# signedchar: int = binmap.field("b") +# unsignedchar: int = binmap.field("B") +# boolean: bool = binmap.field("?") +# short: int = binmap.field("h") +# unsignedshort: int = binmap.field("H") +# integer: int = binmap.field("i") +# unsignedint: int = binmap.field("I") +# long: int = binmap.field("l") +# unsignedlong: int = binmap.field("L") +# longlong: int = binmap.field("q") +# unsignedlonglong: int = binmap.field("Q") +# halffloat: float = binmap.field("e") +# floating: float = binmap.field("f") +# double: float = binmap.field("d") +# string: bytes = binmap.field("10s") +# pascalstring: bytes = binmap.field("15p") +# +# +# class TestAllDatatypes: +# def test_create_class(self): +# sc = AllDatatypes() +# assert sc +# +# def test_with_arguments(self): +# sc = AllDatatypes( +# char=b"%", +# signedchar=-2, +# unsignedchar=5, +# boolean=True, +# short=-7, +# unsignedshort=17, +# integer=-15, +# unsignedint=11, +# long=-2312, +# unsignedlong=2212, +# longlong=-1212, +# unsignedlonglong=4444, +# halffloat=3.5, +# floating=3e3, +# double=13e23, +# string=b"helloworld", +# pascalstring=b"hello pascal", +# ) +# assert sc.char == b"%" +# assert sc.signedchar == -2 +# assert sc.unsignedchar == 5 +# assert sc.boolean +# assert sc.short == -7 +# assert sc.unsignedshort == 17 +# assert sc.integer == -15 +# assert sc.unsignedint == 11 +# assert sc.long == -2312 +# assert sc.unsignedlong == 2212 +# assert sc.longlong == -1212 +# assert sc.unsignedlonglong == 4444 +# assert sc.halffloat == 3.5 +# assert sc.floating == 3e3 +# assert sc.double == 13e23 +# assert sc.string == b"helloworld" +# assert sc.pascalstring == b"hello pascal" +# assert ( +# bytes(sc) +# == b"\x00%\xfe\x05\x01\xff\xf9\x00\x11\xff\xff\xff\xf1\x00\x00\x00\x0b\xff\xff\xf6\xf8\x00\x00" +# b"\x08\xa4\xff\xff\xff\xff\xff\xff\xfbD\x00\x00\x00\x00\x00\x00\x11\\C\x00E;\x80\x00D\xf14\x92Bg\x0c" +# b"\xe8helloworld\x0chello pascal\x00\x00" +# ) +# +# def test_with_binarydata(self): +# sc = AllDatatypes( +# b"\x00W\xee\x15\x00\xf4\xf9\x10\x11\xff\xff\xff1\x00\x00\x01\x0b\xff\xff\xe6\xf8\x00\x00\x18" +# b"\xa4\xff\xff\xff\xff\xff\xff\xfbE\x00\x00\x00\x00\x00\x01\x11\\C\x01E;\x81\x00D\xf14\xa2Bg\x0c" +# b"\xe8hi world \x09hi pascal\x00\x00\x00\x00\x00" +# ) +# assert sc.char == b"W" +# assert sc.signedchar == -18 +# assert sc.unsignedchar == 21 +# assert not sc.boolean +# assert sc.short == -2823 +# assert sc.unsignedshort == 4113 +# assert sc.integer == -207 +# assert sc.unsignedint == 267 +# assert sc.long == -6408 +# assert sc.unsignedlong == 6308 +# assert sc.longlong == -1211 +# assert sc.unsignedlonglong == 69980 +# assert sc.halffloat == 3.501953125 +# assert sc.floating == 3000.0625 +# assert sc.double == 1.3000184467440736e24 +# assert sc.string == b"hi world " +# assert sc.pascalstring == b"hi pascal" diff --git a/tests/test_binmap_dataclass.py b/tests/test_binmap_dataclass.py deleted file mode 100644 index ccf4b89..0000000 --- a/tests/test_binmap_dataclass.py +++ /dev/null @@ -1,426 +0,0 @@ -from dataclasses import dataclass -import struct - -import pytest - -import binmap - - -def test_baseclass(): - b = binmap.BinmapDataclass() - assert type(b) == binmap.BinmapDataclass - assert str(b) == "BinmapDataclass()" - - -def test_baseclass_with_keyword(): - with pytest.raises(TypeError) as excinfo: - binmap.BinmapDataclass(temp=10) - assert "got an unexpected keyword argument 'temp'" in str(excinfo) - - -@dataclass -class Temp(binmap.BinmapDataclass): - temp: int = binmap.field("B") - - -@dataclass -class TempHum(binmap.BinmapDataclass): - temp: int = binmap.field("B") - humidity: int = binmap.field("B") - - -def test_different_classes_eq(): - t = Temp(temp=10) - th = TempHum(temp=10, humidity=60) - assert t != th - assert t.temp == th.temp - - -@dataclass -class Bigendian(binmap.BinmapDataclass): - value: int = binmap.field("q") - - -@dataclass -class Littleedian(binmap.BinmapDataclass, byteorder="<"): - value: int = binmap.field("q") - - -def test_dataformats(): - be = Bigendian(value=-10) - le = Littleedian(value=-10) - - assert be.value == le.value - assert bytes(be) == b"\xff\xff\xff\xff\xff\xff\xff\xf6" - assert bytes(le) == b"\xf6\xff\xff\xff\xff\xff\xff\xff" - - assert bytes(be) != bytes(le) - - be.frombytes(b"\xff\xff\xff\xff\xf4\xff\xff\xf6") - le.frombytes(b"\xff\xff\xff\xff\xf4\xff\xff\xf6") - - assert be.value == -184549386 - assert le.value == -648518393585991681 - - assert be.value != le.value - assert bytes(be) == bytes(le) - - -class TestTempClass: - def test_with_argument(self): - t = Temp(temp=10) - assert t.temp == 10 - assert bytes(t) == b"\x0a" - assert str(t) == "Temp(temp=10)" - - def test_without_argument(self): - t = Temp() - assert t.temp == 0 - assert bytes(t) == b"\x00" - - def test_unknown_argument(self): - with pytest.raises(TypeError) as excinfo: - Temp(hum=60) - assert "got an unexpected keyword argument 'hum'" in str(excinfo) - - def test_value(self): - t = Temp() - t.temp = 10 - assert bytes(t) == b"\x0a" - - def test_raw(self): - t = Temp(b"\x0a") - assert t.temp == 10 - - def test_update_binarydata(self): - t = Temp(b"\x0a") - assert t.temp == 10 - t.frombytes(b"\x14") - assert t.temp == 20 - - def test_change_value(self): - t = Temp(temp=10) - assert bytes(t) == b"\x0a" - - t.temp = 20 - assert bytes(t) == b"\x14" - - def test_value_bounds(self): - t = Temp() - with pytest.raises(struct.error) as excinfo: - t.temp = 256 - assert "ubyte format requires 0 <= number <= 255" in str(excinfo) - - with pytest.raises(struct.error) as excinfo: - t.temp = -1 - assert "ubyte format requires 0 <= number <= 255" in str(excinfo) - - def test_compare_equal(self): - t1 = Temp(temp=10) - t2 = Temp(temp=10) - assert t1.temp == t2.temp - assert t1 == t2 - - def test_compare_not_equal(self): - t1 = Temp(temp=10) - t2 = Temp(temp=20) - assert t1.temp != t2.temp - assert t1 != t2 - - -class TestTempHumClass: - def test_with_argument(self): - th = TempHum(temp=10, humidity=60) - assert th.temp == 10 - assert th.humidity == 60 - assert str(th) == "TempHum(temp=10, humidity=60)" - - def test_without_argument(self): - th = TempHum() - assert th.temp == 0 - assert th.humidity == 0 - assert bytes(th) == b"\x00\x00" - - def test_raw(self): - th = TempHum(b"\x0a\x46") - assert th.temp == 10 - assert th.humidity == 70 - - def test_change_values(self): - th = TempHum(temp=10, humidity=70) - th.temp = 30 - th.humidity = 30 - assert th.temp == 30 - assert th.humidity == 30 - assert bytes(th) == b"\x1e\x1e" - - def test_compare_equal(self): - th1 = TempHum(temp=10, humidity=70) - th2 = TempHum(temp=10, humidity=70) - assert th1.temp == th2.temp - assert th1 == th2 - - def test_compare_not_equal(self): - th1 = TempHum(temp=10, humidity=70) - th2 = TempHum(temp=20, humidity=60) - th3 = TempHum(temp=10, humidity=60) - th4 = TempHum(temp=20, humidity=70) - assert (th1.temp != th2.temp) and (th1.humidity != th2.humidity) - assert th1 != th2 - assert th1 != th3 - assert th1 != th4 - assert th2 != th3 - assert th2 != th4 - - -@dataclass -class Pad(binmap.BinmapDataclass): - temp: int = binmap.field("B") - pad: any = binmap.padding(2) - humidity: int = binmap.field("B") - - -@dataclass -class AdvancedPad(binmap.BinmapDataclass): - temp: int = binmap.field("B") - _pad1: any = binmap.padding(2) - humidity: int = binmap.field("B") - _pad2: any = binmap.padding(3) - _pad3: any = binmap.padding(1) - - -class TestPadClass: - def test_create_pad(self): - p = Pad(temp=10, humidity=60) - with pytest.raises(AttributeError) as excinfo: - p.pad - assert "Padding (pad) is not readable" in str(excinfo) - assert p.temp == 10 - assert p.humidity == 60 - assert str(p) == "Pad(temp=10, humidity=60)" - - def test_parse_data(self): - p = Pad(b"\x0a\x10\x20\x3c") - with pytest.raises(AttributeError) as excinfo: - p._pad - assert p.temp == 10 - assert "Padding (_pad) is not readable" in str(excinfo) - assert p.humidity == 60 - - def test_pack_data(self): - p = Pad() - p.temp = 10 - p.humidity = 60 - assert bytes(p) == b"\x0a\x00\x00\x3c" - - def test_advanced_pad(self): - p = AdvancedPad(temp=10, humidity=60) - with pytest.raises(AttributeError) as excinfo: - p._pad1 - assert "Padding (_pad1) is not readable" in str(excinfo) - with pytest.raises(AttributeError) as excinfo: - p._pad2 - assert "Padding (_pad1) is not readable" in str(excinfo) - with pytest.raises(AttributeError) as excinfo: - p._pad3 - assert "Padding (_pad1) is not readable" in str(excinfo) - assert p.temp == 10 - assert p.humidity == 60 - - def test_advanced_parse_data(self): - p = AdvancedPad(b"\n\x00\x00<\x00\x00\x00\x00") - with pytest.raises(AttributeError) as excinfo: - p._pad1 - assert "Padding (_pad1) is not readable" in str(excinfo) - with pytest.raises(AttributeError) as excinfo: - p._pad2 - assert "Padding (_pad2) is not readable" in str(excinfo) - assert p.humidity == 60 - assert p.temp == 10 - assert str(p) == "AdvancedPad(temp=10, humidity=60)" - - def test_advanced_pack_data(self): - p = AdvancedPad() - p.temp = 10 - p.humidity = 60 - assert bytes(p) == b"\n\x00\x00<\x00\x00\x00\x00" - - -@dataclass -class EnumClass(binmap.BinmapDataclass): - temp: int = binmap.field("B") - wind: int = binmap.enum("h", {0: "North", 1: "East", 2: "South", 4: "West"}) - - -@pytest.mark.skip(reason="Enums need to be redesigned") -class TestEnumClass: - def test_create_class(self): - ec = EnumClass() - assert ec - assert EnumClass.SOUTH == 2 - - def test_get_enum(self): - ec = EnumClass(temp=10, wind=2) - assert ec.wind == "South" - assert str(ec) == "EnumClass, temp=10, wind=South" - - def test_enum_binary(self): - ec = EnumClass(b"\x0a\x02") - assert ec.wind == "South" - assert str(ec) == "EnumClass, temp=10, wind=South" - - def test_set_named_enum(self): - ec = EnumClass() - ec.wind = "South" - assert bytes(ec) == b"\x00\x02" - - with pytest.raises(ValueError) as excinfo: - ec.wind = "Norhtwest" - assert "Unknown enum or value" in str(excinfo) - - with pytest.raises(ValueError) as excinfo: - ec.wind = 1.2 - assert "Unknown enum or value" in str(excinfo) - - def test_colliding_enums(self): - with pytest.raises(ValueError) as excinfo: - - class EnumCollide(binmap.BinmapDataclass): - _datafields = { - "wind1": "B", - "wind2": "B", - } - _enums = { - "wind1": {0: "North"}, - "wind2": {2: "North"}, - } - - assert "North already defined" in str(excinfo) - - -@dataclass -class ConstValues(binmap.BinmapDataclass): - datatype: int = binmap.const("B", 0x15) - status: int = binmap.field("B") - - -class TestConstValues: - def test_create_class(self): - c = ConstValues() - with pytest.raises(AttributeError) as excinfo: - ConstValues(datatype=0x14, status=1) - assert "datatype is a constant" in str(excinfo) - assert c.datatype == 0x15 - - def test_set_value(self): - c = ConstValues(status=1) - with pytest.raises(AttributeError) as excinfo: - c.datatype = 0x14 - assert "datatype is a constant" in str(excinfo) - assert c.datatype == 0x15 - assert c.status == 1 - assert bytes(c) == b"\x15\x01" - - def test_binary_data(self): - c = ConstValues(b"\x15\x01") - with pytest.raises(ValueError) as excinfo: - ConstValues(b"\x14\x01") - assert "Constant doesn't match binary data" in str(excinfo) - assert c.datatype == 0x15 - assert c.status == 1 - - -@dataclass -class AllDatatypes(binmap.BinmapDataclass): - _pad: any = binmap.padding(1) - char: int = binmap.field("c") - signedchar: int = binmap.field("b") - unsignedchar: int = binmap.field("B") - boolean: bool = binmap.field("?") - short: int = binmap.field("h") - unsignedshort: int = binmap.field("H") - integer: int = binmap.field("i") - unsignedint: int = binmap.field("I") - long: int = binmap.field("l") - unsignedlong: int = binmap.field("L") - longlong: int = binmap.field("q") - unsignedlonglong: int = binmap.field("Q") - halffloat: float = binmap.field("e") - floating: float = binmap.field("f") - double: float = binmap.field("d") - string: bytes = binmap.field("10s") - pascalstring: bytes = binmap.field("15p") - - -class TestAllDatatypes: - def test_create_class(self): - sc = AllDatatypes() - assert sc - - def test_with_arguments(self): - sc = AllDatatypes( - char=b"%", - signedchar=-2, - unsignedchar=5, - boolean=True, - short=-7, - unsignedshort=17, - integer=-15, - unsignedint=11, - long=-2312, - unsignedlong=2212, - longlong=-1212, - unsignedlonglong=4444, - halffloat=3.5, - floating=3e3, - double=13e23, - string=b"helloworld", - pascalstring=b"hello pascal", - ) - assert sc.char == b"%" - assert sc.signedchar == -2 - assert sc.unsignedchar == 5 - assert sc.boolean - assert sc.short == -7 - assert sc.unsignedshort == 17 - assert sc.integer == -15 - assert sc.unsignedint == 11 - assert sc.long == -2312 - assert sc.unsignedlong == 2212 - assert sc.longlong == -1212 - assert sc.unsignedlonglong == 4444 - assert sc.halffloat == 3.5 - assert sc.floating == 3e3 - assert sc.double == 13e23 - assert sc.string == b"helloworld" - assert sc.pascalstring == b"hello pascal" - assert ( - bytes(sc) - == b"\x00%\xfe\x05\x01\xff\xf9\x00\x11\xff\xff\xff\xf1\x00\x00\x00\x0b\xff\xff\xf6\xf8\x00\x00" - b"\x08\xa4\xff\xff\xff\xff\xff\xff\xfbD\x00\x00\x00\x00\x00\x00\x11\\C\x00E;\x80\x00D\xf14\x92Bg\x0c" - b"\xe8helloworld\x0chello pascal\x00\x00" - ) - - def test_with_binarydata(self): - sc = AllDatatypes( - b"\x00W\xee\x15\x00\xf4\xf9\x10\x11\xff\xff\xff1\x00\x00\x01\x0b\xff\xff\xe6\xf8\x00\x00\x18" - b"\xa4\xff\xff\xff\xff\xff\xff\xfbE\x00\x00\x00\x00\x00\x01\x11\\C\x01E;\x81\x00D\xf14\xa2Bg\x0c" - b"\xe8hi world \x09hi pascal\x00\x00\x00\x00\x00" - ) - assert sc.char == b"W" - assert sc.signedchar == -18 - assert sc.unsignedchar == 21 - assert not sc.boolean - assert sc.short == -2823 - assert sc.unsignedshort == 4113 - assert sc.integer == -207 - assert sc.unsignedint == 267 - assert sc.long == -6408 - assert sc.unsignedlong == 6308 - assert sc.longlong == -1211 - assert sc.unsignedlonglong == 69980 - assert sc.halffloat == 3.501953125 - assert sc.floating == 3000.0625 - assert sc.double == 1.3000184467440736e24 - assert sc.string == b"hi world " - assert sc.pascalstring == b"hi pascal" From 0cadc31a9e58fb6bca6a71b5ce892136f6f39fc1 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Mon, 2 Mar 2020 15:31:29 +0100 Subject: [PATCH 03/18] Move dataclass creation to binmapdataclass. --- binmap/__init__.py | 2 +- tests/test_binmap.py | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/binmap/__init__.py b/binmap/__init__.py index 638de91..8f0a818 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -89,7 +89,7 @@ def __set__(self, obj, value): def binmapdataclass(cls: Type[T]) -> Type[T]: - + dataclasses.dataclass(cls) type_hints = get_type_hints(cls) cls._formatstring = "" diff --git a/tests/test_binmap.py b/tests/test_binmap.py index fd5f0e7..0d8c251 100644 --- a/tests/test_binmap.py +++ b/tests/test_binmap.py @@ -19,13 +19,11 @@ def test_baseclass_with_keyword(): @binmap.binmapdataclass -@dataclass class Temp(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 @binmap.binmapdataclass -@dataclass class TempHum(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 humidity: binmap.unsignedchar = 0 @@ -39,13 +37,11 @@ def test_different_classes_eq(): @binmap.binmapdataclass -@dataclass class Bigendian(binmap.BinmapDataclass): value: binmap.longlong = 0 @binmap.binmapdataclass -@dataclass class Littleedian(binmap.BinmapDataclass, byteorder="<"): value: binmap.longlong = 0 @@ -178,7 +174,6 @@ def test_compare_not_equal(self): @binmap.binmapdataclass -@dataclass class Pad(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 pad: binmap.padding = 2 @@ -186,7 +181,6 @@ class Pad(binmap.BinmapDataclass): @binmap.binmapdataclass -@dataclass class AdvancedPad(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 _pad1: binmap.padding = 2 @@ -253,7 +247,6 @@ def test_advanced_pack_data(self): @binmap.binmapdataclass -@dataclass class EnumClass(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 # wind: int = binmap.enum("h", {0: "North", 1: "East", 2: "South", 4: "West"}) @@ -306,7 +299,6 @@ class EnumCollide(binmap.BinmapDataclass): @binmap.binmapdataclass -@dataclass class ConstValues(binmap.BinmapDataclass): datatype: binmap.constant[binmap.unsignedchar] = 0x15 status: binmap.unsignedchar = 0 From b1f5aef498e805e5d3467bdfd5fcfc4519632bca Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Mon, 2 Mar 2020 16:30:33 +0100 Subject: [PATCH 04/18] Make pading behave. --- binmap/__init__.py | 12 ++++++++---- tests/test_binmap.py | 12 ++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/binmap/__init__.py b/binmap/__init__.py index 8f0a818..74d6e96 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -77,13 +77,13 @@ def __set__(self, obj, value): unsignedchar = NewType("unsingedchar", int) longlong = NewType("longlong", int) -padding = NewType("padding", int) +pad = NewType("pad", int) constant = Tuple datatypemapping: Dict[type, Tuple[BaseDescriptor, str]] = { unsignedchar: (BinField, "B"), longlong: (BinField, "q"), - padding: (PaddingField, "x"), + pad: (PaddingField, "x"), constant: (ConstField, "B"), } @@ -101,13 +101,17 @@ def binmapdataclass(cls: Type[T]) -> Type[T]: else: _base, _type = datatypemapping[type_hints[field_.name]] setattr(cls, field_.name, _base(name=field_.name)) - if type_hints[field_.name] is padding: + if type_hints[field_.name] is pad: _type = field_.default * _type cls._formatstring += _type return cls +def padding(length=1): + return dataclasses.field(default=length, repr=False) + + @dataclasses.dataclass class BinmapDataclass(ABC): _byteorder = "" @@ -137,7 +141,7 @@ def _unpacker(self, value): datafields = [ f.name for f in dataclasses.fields(self) - if not (type_hints[f.name] is padding) + if not (type_hints[f.name] is pad) and not (getattr(f.type, "__origin__", None)) ] args = struct.unpack(self._byteorder + self._formatstring, value) diff --git a/tests/test_binmap.py b/tests/test_binmap.py index 0d8c251..d145710 100644 --- a/tests/test_binmap.py +++ b/tests/test_binmap.py @@ -176,17 +176,17 @@ def test_compare_not_equal(self): @binmap.binmapdataclass class Pad(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 - pad: binmap.padding = 2 + pad: binmap.pad = binmap.padding(2) humidity: binmap.unsignedchar = 0 @binmap.binmapdataclass class AdvancedPad(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 - _pad1: binmap.padding = 2 + _pad1: binmap.pad = binmap.padding(2) humidity: binmap.unsignedchar = 0 - _pad2: binmap.padding = 3 - _pad3: binmap.padding = 1 + _pad2: binmap.pad = binmap.padding(3) + _pad3: binmap.pad = binmap.padding(1) class TestPadClass: @@ -220,10 +220,10 @@ def test_advanced_pad(self): assert "Padding (_pad1) is not readable" in str(excinfo) with pytest.raises(AttributeError) as excinfo: p._pad2 - assert "Padding (_pad1) is not readable" in str(excinfo) + assert "Padding (_pad2) is not readable" in str(excinfo) with pytest.raises(AttributeError) as excinfo: p._pad3 - assert "Padding (_pad1) is not readable" in str(excinfo) + assert "Padding (_pad3) is not readable" in str(excinfo) assert p.temp == 10 assert p.humidity == 60 From d32ee75bf2421f9a8701b1bfe70bac1787437700 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Tue, 3 Mar 2020 00:05:41 +0100 Subject: [PATCH 05/18] Make constants work --- binmap/__init__.py | 36 ++++++++++++++++++++++++------------ tests/test_binmap.py | 8 +++++--- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/binmap/__init__.py b/binmap/__init__.py index 74d6e96..7aad3ce 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -78,13 +78,11 @@ def __set__(self, obj, value): unsignedchar = NewType("unsingedchar", int) longlong = NewType("longlong", int) pad = NewType("pad", int) -constant = Tuple datatypemapping: Dict[type, Tuple[BaseDescriptor, str]] = { unsignedchar: (BinField, "B"), longlong: (BinField, "q"), pad: (PaddingField, "x"), - constant: (ConstField, "B"), } @@ -93,13 +91,11 @@ def binmapdataclass(cls: Type[T]) -> Type[T]: type_hints = get_type_hints(cls) cls._formatstring = "" - # Used to list all fields and locate fields by field number. + for field_ in dataclasses.fields(cls): - if getattr(field_.type, "__origin__", None) is tuple: - _, _type = datatypemapping[field_.type.__args__[0]] + _base, _type = datatypemapping[type_hints[field_.name]] + if "constant" in field_.metadata: _base = ConstField - else: - _base, _type = datatypemapping[type_hints[field_.name]] setattr(cls, field_.name, _base(name=field_.name)) if type_hints[field_.name] is pad: _type = field_.default * _type @@ -109,7 +105,11 @@ def binmapdataclass(cls: Type[T]) -> Type[T]: def padding(length=1): - return dataclasses.field(default=length, repr=False) + return dataclasses.field(default=length, repr=False, metadata={"padding": True}) + + +def constant(value): + return dataclasses.field(default=value, init=False, metadata={"constant": True}) @dataclasses.dataclass @@ -135,17 +135,29 @@ def __bytes__(self): def __post_init__(self, _binarydata): if _binarydata != b"": self._unpacker(_binarydata) + # Kludgy hack to keep order + for f in dataclasses.fields(self): + if "padding" in f.metadata: + continue + if "constant" in f.metadata: + self.__dict__.update({f.name: f.default}) + else: + val = getattr(self, f.name) + del self.__dict__[f.name] + self.__dict__.update({f.name: val}) def _unpacker(self, value): type_hints = get_type_hints(self) + datafieldsmap = {f.name: f for f in dataclasses.fields(self)} datafields = [ - f.name - for f in dataclasses.fields(self) - if not (type_hints[f.name] is pad) - and not (getattr(f.type, "__origin__", None)) + f.name for f in dataclasses.fields(self) if not (type_hints[f.name] is pad) ] args = struct.unpack(self._byteorder + self._formatstring, value) for arg, name in zip(args, datafields): + if "constant" in datafieldsmap[name].metadata: + if arg != datafieldsmap[name].default: + raise ValueError("Constant doesn't match binary data") + setattr(self, name, arg) def frombytes(self, value): diff --git a/tests/test_binmap.py b/tests/test_binmap.py index d145710..ca55a24 100644 --- a/tests/test_binmap.py +++ b/tests/test_binmap.py @@ -300,16 +300,18 @@ class EnumCollide(binmap.BinmapDataclass): @binmap.binmapdataclass class ConstValues(binmap.BinmapDataclass): - datatype: binmap.constant[binmap.unsignedchar] = 0x15 + datatype: binmap.unsignedchar = binmap.constant(0x15) status: binmap.unsignedchar = 0 class TestConstValues: def test_create_class(self): c = ConstValues() - with pytest.raises(AttributeError) as excinfo: + with pytest.raises(TypeError) as excinfo: ConstValues(datatype=0x14, status=1) - assert "datatype is a constant" in str(excinfo) + assert "__init__() got an unexpected keyword argument 'datatype'" in str( + excinfo + ) assert c.datatype == 0x15 def test_set_value(self): From 269ac31a5c8e1dfabb7df674ec7980ff5adf1fae Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Tue, 3 Mar 2020 00:53:20 +0100 Subject: [PATCH 06/18] Added all data types. --- binmap/__init__.py | 51 +++++++++++- tests/test_binmap.py | 190 +++++++++++++++++++++---------------------- 2 files changed, 144 insertions(+), 97 deletions(-) diff --git a/binmap/__init__.py b/binmap/__init__.py index 7aad3ce..135cfa0 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -75,13 +75,48 @@ def __set__(self, obj, value): obj.__dict__.update({self.name: value}) +char = NewType("char", int) +signedchar = NewType("signedchar", int) unsignedchar = NewType("unsingedchar", int) +boolean = NewType("boolean", bool) +short = NewType("short", int) +unsignedshort = NewType("short", int) +integer = NewType("integer", int) +unsignedinteger = NewType("unsignedinteger", int) +long = NewType("long", int) +unsignedlong = NewType("unsignedlog", int) longlong = NewType("longlong", int) +unsignedlonglong = NewType("longlong", int) +halffloat = NewType("halffloat", float) +floating = NewType("floating", float) +double = NewType("double", float) +string = NewType("string", str) +pascalstring = NewType("pascalstring", str) pad = NewType("pad", int) datatypemapping: Dict[type, Tuple[BaseDescriptor, str]] = { + char: (BinField, "c"), + chr: (BinField, "c"), + signedchar: (BinField, "b"), unsignedchar: (BinField, "B"), + boolean: (BinField, "?"), + bool: (BinField, "?"), + short: (BinField, "h"), + unsignedshort: (BinField, "H"), + integer: (BinField, "i"), + int: (BinField, "i"), + unsignedinteger: (BinField, "I"), + long: (BinField, "l"), + unsignedlong: (BinField, "L"), longlong: (BinField, "q"), + unsignedlonglong: (BinField, "Q"), + halffloat: (BinField, "e"), + floating: (BinField, "f"), + float: (BinField, "f"), + double: (BinField, "d"), + string: (BinField, "s"), + str: (BinField, "s"), + pascalstring: (BinField, "p"), pad: (PaddingField, "x"), } @@ -99,12 +134,18 @@ def binmapdataclass(cls: Type[T]) -> Type[T]: setattr(cls, field_.name, _base(name=field_.name)) if type_hints[field_.name] is pad: _type = field_.default * _type + if ( + type_hints[field_.name] is string + or type_hints[field_.name] is pascalstring + or type_hints[field_.name] is str + ): + _type = str(field_.metadata["length"]) + _type cls._formatstring += _type return cls -def padding(length=1): +def padding(length: int = 1): return dataclasses.field(default=length, repr=False, metadata={"padding": True}) @@ -112,6 +153,14 @@ def constant(value): return dataclasses.field(default=value, init=False, metadata={"constant": True}) +def stringfield(length: int = 1): + return dataclasses.field(default=b"\x00" * length, metadata={"length": length}) + + +def pascalstringfield(length=1): + return dataclasses.field(default=b"\x00" * length, metadata={"length": length}) + + @dataclasses.dataclass class BinmapDataclass(ABC): _byteorder = "" diff --git a/tests/test_binmap.py b/tests/test_binmap.py index ca55a24..c7d9c32 100644 --- a/tests/test_binmap.py +++ b/tests/test_binmap.py @@ -1,5 +1,4 @@ import struct -from dataclasses import dataclass import pytest @@ -332,98 +331,97 @@ def test_binary_data(self): assert c.status == 1 -# @binmap.binmapdataclass -# @dataclass -# class AllDatatypes(binmap.BinmapDataclass): -# _pad: any = binmap.padding(1) -# char: int = binmap.field("c") -# signedchar: int = binmap.field("b") -# unsignedchar: int = binmap.field("B") -# boolean: bool = binmap.field("?") -# short: int = binmap.field("h") -# unsignedshort: int = binmap.field("H") -# integer: int = binmap.field("i") -# unsignedint: int = binmap.field("I") -# long: int = binmap.field("l") -# unsignedlong: int = binmap.field("L") -# longlong: int = binmap.field("q") -# unsignedlonglong: int = binmap.field("Q") -# halffloat: float = binmap.field("e") -# floating: float = binmap.field("f") -# double: float = binmap.field("d") -# string: bytes = binmap.field("10s") -# pascalstring: bytes = binmap.field("15p") -# -# -# class TestAllDatatypes: -# def test_create_class(self): -# sc = AllDatatypes() -# assert sc -# -# def test_with_arguments(self): -# sc = AllDatatypes( -# char=b"%", -# signedchar=-2, -# unsignedchar=5, -# boolean=True, -# short=-7, -# unsignedshort=17, -# integer=-15, -# unsignedint=11, -# long=-2312, -# unsignedlong=2212, -# longlong=-1212, -# unsignedlonglong=4444, -# halffloat=3.5, -# floating=3e3, -# double=13e23, -# string=b"helloworld", -# pascalstring=b"hello pascal", -# ) -# assert sc.char == b"%" -# assert sc.signedchar == -2 -# assert sc.unsignedchar == 5 -# assert sc.boolean -# assert sc.short == -7 -# assert sc.unsignedshort == 17 -# assert sc.integer == -15 -# assert sc.unsignedint == 11 -# assert sc.long == -2312 -# assert sc.unsignedlong == 2212 -# assert sc.longlong == -1212 -# assert sc.unsignedlonglong == 4444 -# assert sc.halffloat == 3.5 -# assert sc.floating == 3e3 -# assert sc.double == 13e23 -# assert sc.string == b"helloworld" -# assert sc.pascalstring == b"hello pascal" -# assert ( -# bytes(sc) -# == b"\x00%\xfe\x05\x01\xff\xf9\x00\x11\xff\xff\xff\xf1\x00\x00\x00\x0b\xff\xff\xf6\xf8\x00\x00" -# b"\x08\xa4\xff\xff\xff\xff\xff\xff\xfbD\x00\x00\x00\x00\x00\x00\x11\\C\x00E;\x80\x00D\xf14\x92Bg\x0c" -# b"\xe8helloworld\x0chello pascal\x00\x00" -# ) -# -# def test_with_binarydata(self): -# sc = AllDatatypes( -# b"\x00W\xee\x15\x00\xf4\xf9\x10\x11\xff\xff\xff1\x00\x00\x01\x0b\xff\xff\xe6\xf8\x00\x00\x18" -# b"\xa4\xff\xff\xff\xff\xff\xff\xfbE\x00\x00\x00\x00\x00\x01\x11\\C\x01E;\x81\x00D\xf14\xa2Bg\x0c" -# b"\xe8hi world \x09hi pascal\x00\x00\x00\x00\x00" -# ) -# assert sc.char == b"W" -# assert sc.signedchar == -18 -# assert sc.unsignedchar == 21 -# assert not sc.boolean -# assert sc.short == -2823 -# assert sc.unsignedshort == 4113 -# assert sc.integer == -207 -# assert sc.unsignedint == 267 -# assert sc.long == -6408 -# assert sc.unsignedlong == 6308 -# assert sc.longlong == -1211 -# assert sc.unsignedlonglong == 69980 -# assert sc.halffloat == 3.501953125 -# assert sc.floating == 3000.0625 -# assert sc.double == 1.3000184467440736e24 -# assert sc.string == b"hi world " -# assert sc.pascalstring == b"hi pascal" +@binmap.binmapdataclass +class AllDatatypes(binmap.BinmapDataclass): + _pad: binmap.pad = binmap.padding(1) + char: binmap.char = b"\x00" + signedchar: binmap.signedchar = 0 + unsignedchar: binmap.unsignedchar = 0 + boolean: binmap.boolean = False + short: binmap.short = 0 + unsignedshort: binmap.unsignedshort = 0 + integer: binmap.integer = 0 + unsignedint: binmap.unsignedinteger = 0 + long: binmap.long = 0 + unsignedlong: binmap.unsignedlong = 0 + longlong: binmap.longlong = 0 + unsignedlonglong: binmap.unsignedlonglong = 0 + halffloat: binmap.halffloat = 0.0 + floating: binmap.floating = 0.0 + double: binmap.double = 0.0 + string: binmap.string = binmap.stringfield(10) + pascalstring: binmap.pascalstring = binmap.pascalstringfield(15) + + +class TestAllDatatypes: + def test_create_class(self): + sc = AllDatatypes() + assert sc + + def test_with_arguments(self): + sc = AllDatatypes( + char=b"%", + signedchar=-2, + unsignedchar=5, + boolean=True, + short=-7, + unsignedshort=17, + integer=-15, + unsignedint=11, + long=-2312, + unsignedlong=2212, + longlong=-1212, + unsignedlonglong=4444, + halffloat=3.5, + floating=3e3, + double=13e23, + string=b"helloworld", + pascalstring=b"hello pascal", + ) + assert sc.char == b"%" + assert sc.signedchar == -2 + assert sc.unsignedchar == 5 + assert sc.boolean + assert sc.short == -7 + assert sc.unsignedshort == 17 + assert sc.integer == -15 + assert sc.unsignedint == 11 + assert sc.long == -2312 + assert sc.unsignedlong == 2212 + assert sc.longlong == -1212 + assert sc.unsignedlonglong == 4444 + assert sc.halffloat == 3.5 + assert sc.floating == 3e3 + assert sc.double == 13e23 + assert sc.string == b"helloworld" + assert sc.pascalstring == b"hello pascal" + assert ( + bytes(sc) + == b"\x00%\xfe\x05\x01\xff\xf9\x00\x11\xff\xff\xff\xf1\x00\x00\x00\x0b\xff\xff\xf6\xf8\x00\x00" + b"\x08\xa4\xff\xff\xff\xff\xff\xff\xfbD\x00\x00\x00\x00\x00\x00\x11\\C\x00E;\x80\x00D\xf14\x92Bg\x0c" + b"\xe8helloworld\x0chello pascal\x00\x00" + ) + + def test_with_binarydata(self): + sc = AllDatatypes( + b"\x00W\xee\x15\x00\xf4\xf9\x10\x11\xff\xff\xff1\x00\x00\x01\x0b\xff\xff\xe6\xf8\x00\x00\x18" + b"\xa4\xff\xff\xff\xff\xff\xff\xfbE\x00\x00\x00\x00\x00\x01\x11\\C\x01E;\x81\x00D\xf14\xa2Bg\x0c" + b"\xe8hi world \x09hi pascal\x00\x00\x00\x00\x00" + ) + assert sc.char == b"W" + assert sc.signedchar == -18 + assert sc.unsignedchar == 21 + assert not sc.boolean + assert sc.short == -2823 + assert sc.unsignedshort == 4113 + assert sc.integer == -207 + assert sc.unsignedint == 267 + assert sc.long == -6408 + assert sc.unsignedlong == 6308 + assert sc.longlong == -1211 + assert sc.unsignedlonglong == 69980 + assert sc.halffloat == 3.501953125 + assert sc.floating == 3000.0625 + assert sc.double == 1.3000184467440736e24 + assert sc.string == b"hi world " + assert sc.pascalstring == b"hi pascal" From 26d13fc05340231937716f58d672be527ac65cd1 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Tue, 3 Mar 2020 01:29:55 +0100 Subject: [PATCH 07/18] Added some typing. --- binmap/__init__.py | 39 ++++++++++++++++++++------------------- tests/test_binmap.py | 2 +- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/binmap/__init__.py b/binmap/__init__.py index 135cfa0..b84d7ab 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -1,7 +1,7 @@ import dataclasses import struct from abc import ABC -from typing import Dict, NewType, Tuple, Type, TypeVar, get_type_hints +from typing import Dict, NewType, Tuple, Type, TypeVar, Union, get_type_hints T = TypeVar("T") @@ -77,16 +77,16 @@ def __set__(self, obj, value): char = NewType("char", int) signedchar = NewType("signedchar", int) -unsignedchar = NewType("unsingedchar", int) +unsignedchar = NewType("unsignedchar", int) boolean = NewType("boolean", bool) short = NewType("short", int) -unsignedshort = NewType("short", int) +unsignedshort = NewType("unsignedshort", int) integer = NewType("integer", int) unsignedinteger = NewType("unsignedinteger", int) long = NewType("long", int) -unsignedlong = NewType("unsignedlog", int) +unsignedlong = NewType("unsignedlong", int) longlong = NewType("longlong", int) -unsignedlonglong = NewType("longlong", int) +unsignedlonglong = NewType("unsignedlonglong", int) halffloat = NewType("halffloat", float) floating = NewType("floating", float) double = NewType("double", float) @@ -94,9 +94,8 @@ def __set__(self, obj, value): pascalstring = NewType("pascalstring", str) pad = NewType("pad", int) -datatypemapping: Dict[type, Tuple[BaseDescriptor, str]] = { +datatypemapping: Dict[type, Tuple[Type[BaseDescriptor], str]] = { char: (BinField, "c"), - chr: (BinField, "c"), signedchar: (BinField, "b"), unsignedchar: (BinField, "B"), boolean: (BinField, "?"), @@ -145,20 +144,22 @@ def binmapdataclass(cls: Type[T]) -> Type[T]: return cls -def padding(length: int = 1): +def padding(length: int = 1) -> dataclasses.Field: return dataclasses.field(default=length, repr=False, metadata={"padding": True}) -def constant(value): +def constant(value: Union[int, float, str]) -> dataclasses.Field: return dataclasses.field(default=value, init=False, metadata={"constant": True}) -def stringfield(length: int = 1): - return dataclasses.field(default=b"\x00" * length, metadata={"length": length}) - - -def pascalstringfield(length=1): - return dataclasses.field(default=b"\x00" * length, metadata={"length": length}) +def stringfield( + length: int = 1, default: str = "", fillchar: str = " " +) -> dataclasses.Field: + if default == "": + _default = "\x00" * length + else: + _default = f"{default:{fillchar}<{length}}" + return dataclasses.field(default=_default, metadata={"length": length}) @dataclasses.dataclass @@ -167,7 +168,7 @@ class BinmapDataclass(ABC): _formatstring = "" __binarydata: dataclasses.InitVar[bytes] = b"" - def __init_subclass__(cls, byteorder=">"): + def __init_subclass__(cls, byteorder: str = ">"): cls._byteorder = byteorder def __bytes__(self): @@ -181,7 +182,7 @@ def __bytes__(self): ), ) - def __post_init__(self, _binarydata): + def __post_init__(self, _binarydata: bytes): if _binarydata != b"": self._unpacker(_binarydata) # Kludgy hack to keep order @@ -195,7 +196,7 @@ def __post_init__(self, _binarydata): del self.__dict__[f.name] self.__dict__.update({f.name: val}) - def _unpacker(self, value): + def _unpacker(self, value: bytes): type_hints = get_type_hints(self) datafieldsmap = {f.name: f for f in dataclasses.fields(self)} datafields = [ @@ -209,6 +210,6 @@ def _unpacker(self, value): setattr(self, name, arg) - def frombytes(self, value): + def frombytes(self, value: bytes): self._unpacker(value) self._binarydata = value diff --git a/tests/test_binmap.py b/tests/test_binmap.py index c7d9c32..bfb035b 100644 --- a/tests/test_binmap.py +++ b/tests/test_binmap.py @@ -350,7 +350,7 @@ class AllDatatypes(binmap.BinmapDataclass): floating: binmap.floating = 0.0 double: binmap.double = 0.0 string: binmap.string = binmap.stringfield(10) - pascalstring: binmap.pascalstring = binmap.pascalstringfield(15) + pascalstring: binmap.pascalstring = binmap.stringfield(15) class TestAllDatatypes: From 7d7d27f08575192d3300a94ef83f5c379c5a72eb Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Tue, 3 Mar 2020 23:23:10 +0100 Subject: [PATCH 08/18] Add support for enums --- binmap/__init__.py | 35 +++++++++++++++++------------------ tests/test_binmap.py | 43 +++++++++++++++++-------------------------- 2 files changed, 34 insertions(+), 44 deletions(-) diff --git a/binmap/__init__.py b/binmap/__init__.py index b84d7ab..e47378e 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -1,6 +1,7 @@ import dataclasses import struct from abc import ABC +from enum import Enum from typing import Dict, NewType, Tuple, Type, TypeVar, Union, get_type_hints T = TypeVar("T") @@ -28,7 +29,6 @@ def __get__(self, obj, owner): def __set__(self, obj, value): type_hints = get_type_hints(obj) struct.pack(datatypemapping[type_hints[self.name]][1], value) - # struct.pack(obj._datafields[self.name], value) obj.__dict__[self.name] = value @@ -44,25 +44,18 @@ def __set__(self, obj, value): pass -class EnumField(BaseDescriptor): +class EnumField(BinField): """EnumField descriptor uses "enum" to map to and from strings. Accepts both strings and values when setting. Only values that has a corresponding string is allowed.""" - def __get__(self, obj, owner): - value = obj.__dict__[f"_{self.name}"] - return obj._enums[self.name][value] - def __set__(self, obj, value): - if value in obj._enums[self.name]: - obj.__dict__[f"_{self.name}"] = value + datafieldsmap = {f.name: f for f in dataclasses.fields(obj)} + if type(value) is str: + datafieldsmap[self.name].metadata["enum"][value] else: - for k, v in obj._enums[self.name].items(): - if v == value: - obj.__dict__[f"_{self.name}"] = k - return - - raise ValueError("Unknown enum or value") + datafieldsmap[self.name].metadata["enum"](value) + obj.__dict__[self.name] = value class ConstField(BinField): @@ -130,6 +123,8 @@ def binmapdataclass(cls: Type[T]) -> Type[T]: _base, _type = datatypemapping[type_hints[field_.name]] if "constant" in field_.metadata: _base = ConstField + elif "enum" in field_.metadata: + _base = EnumField setattr(cls, field_.name, _base(name=field_.name)) if type_hints[field_.name] is pad: _type = field_.default * _type @@ -153,15 +148,19 @@ def constant(value: Union[int, float, str]) -> dataclasses.Field: def stringfield( - length: int = 1, default: str = "", fillchar: str = " " + length: int = 1, default: bytes = b"", fillchar: bytes = b" " ) -> dataclasses.Field: - if default == "": - _default = "\x00" * length + if default == b"": + _default = b"\x00" * length else: - _default = f"{default:{fillchar}<{length}}" + _default = bytes(f"{default:{fillchar}<{length}}") return dataclasses.field(default=_default, metadata={"length": length}) +def enumfield(enumclass: Enum, default: Enum = None) -> dataclasses.Field: + return dataclasses.field(default=default, metadata={"enum": enumclass}) + + @dataclasses.dataclass class BinmapDataclass(ABC): _byteorder = "" diff --git a/tests/test_binmap.py b/tests/test_binmap.py index bfb035b..5bb8e59 100644 --- a/tests/test_binmap.py +++ b/tests/test_binmap.py @@ -1,4 +1,5 @@ import struct +from enum import IntEnum import pytest @@ -245,56 +246,46 @@ def test_advanced_pack_data(self): assert bytes(p) == b"\n\x00\x00<\x00\x00\x00\x00" +class WindEnum(IntEnum): + North = 0 + East = 1 + South = 2 + West = 3 + + @binmap.binmapdataclass class EnumClass(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 - # wind: int = binmap.enum("h", {0: "North", 1: "East", 2: "South", 4: "West"}) + wind: binmap.unsignedchar = binmap.enumfield(WindEnum, default=WindEnum.East) -@pytest.mark.skip(reason="Enums need to be redesigned") class TestEnumClass: def test_create_class(self): ec = EnumClass() assert ec - assert EnumClass.SOUTH == 2 def test_get_enum(self): ec = EnumClass(temp=10, wind=2) - assert ec.wind == "South" - assert str(ec) == "EnumClass, temp=10, wind=South" + assert ec.wind == WindEnum.South + assert str(ec) == "EnumClass(temp=10, wind=2)" def test_enum_binary(self): ec = EnumClass(b"\x0a\x02") - assert ec.wind == "South" - assert str(ec) == "EnumClass, temp=10, wind=South" + assert ec.wind == WindEnum.South + assert str(ec) == "EnumClass(temp=10, wind=2)" def test_set_named_enum(self): ec = EnumClass() - ec.wind = "South" + ec.wind = WindEnum.South assert bytes(ec) == b"\x00\x02" - with pytest.raises(ValueError) as excinfo: + with pytest.raises(KeyError) as excinfo: ec.wind = "Norhtwest" - assert "Unknown enum or value" in str(excinfo) + assert "'Norhtwest'" in str(excinfo) with pytest.raises(ValueError) as excinfo: ec.wind = 1.2 - assert "Unknown enum or value" in str(excinfo) - - def test_colliding_enums(self): - with pytest.raises(ValueError) as excinfo: - - class EnumCollide(binmap.BinmapDataclass): - _datafields = { - "wind1": "B", - "wind2": "B", - } - _enums = { - "wind1": {0: "North"}, - "wind2": {2: "North"}, - } - - assert "North already defined" in str(excinfo) + assert "1.2 is not a valid WindEnum" in str(excinfo) @binmap.binmapdataclass From 9368dc65f21b3ce09f1807c0c4b806338765ae1f Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Wed, 4 Mar 2020 22:16:14 +0100 Subject: [PATCH 09/18] Updated documentation. --- README.rst | 19 +++++++------- binmap/__init__.py | 64 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index fc146a9..7768a0d 100644 --- a/README.rst +++ b/README.rst @@ -1,15 +1,13 @@ -Class for go to and from binary data +Dataclass for go to and from binary data -It's designed to be easy to create mappings by just having a -``_datafields`` class attribute. - +It follows dataclass pattern with typehinting as the binary format. Temperature with one unsigned byte: .. code-block:: python - - class Temperature(Binmap): - _datafields = {"temp": "B"} + @binmapdataclass + class Temperature(BinmapDataclass): + temp: unsignedchar = 0 t = Temperature() t.temp = 22 @@ -24,9 +22,10 @@ Temperature and humidity consisting of one signed byte for temperature and one unsiged byte for humidity: .. code-block:: python - - class TempHum(Binmap): - _datafields = {"temp": "b", "hum": "B"} + @binmapdataclass + class TempHum(BinmapDataclass): + temp: signedchar = 0 + hum: unsignedchar = 0 th = TempHum() th.temp = -10 diff --git a/binmap/__init__.py b/binmap/__init__.py index e47378e..d9e7139 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -1,7 +1,7 @@ import dataclasses import struct from abc import ABC -from enum import Enum +from enum import IntEnum from typing import Dict, NewType, Tuple, Type, TypeVar, Union, get_type_hints T = TypeVar("T") @@ -114,6 +114,12 @@ def __set__(self, obj, value): def binmapdataclass(cls: Type[T]) -> Type[T]: + """ + Decorator to create dataclass for binmapping + + :param cls: Class to decorate + :return: Decorated class + """ dataclasses.dataclass(cls) type_hints = get_type_hints(cls) @@ -140,16 +146,36 @@ def binmapdataclass(cls: Type[T]) -> Type[T]: def padding(length: int = 1) -> dataclasses.Field: + """ + Field generator function for padding elements + + :param int lenght: Number of bytes of padded field + :return: dataclass field + """ return dataclasses.field(default=length, repr=False, metadata={"padding": True}) def constant(value: Union[int, float, str]) -> dataclasses.Field: + """ + Field generator function for constant elements + + :param value: Constant value for the field. + :return: dataclass field + """ return dataclasses.field(default=value, init=False, metadata={"constant": True}) def stringfield( length: int = 1, default: bytes = b"", fillchar: bytes = b" " ) -> dataclasses.Field: + """ + Field generator function for string fields. + + :param int lenght: lengt of the string. + :param bytes default: default value of the string + :param bytes fillchar: char to pad the string with + :return: dataclass field + """ if default == b"": _default = b"\x00" * length else: @@ -157,20 +183,40 @@ def stringfield( return dataclasses.field(default=_default, metadata={"length": length}) -def enumfield(enumclass: Enum, default: Enum = None) -> dataclasses.Field: +def enumfield(enumclass: IntEnum, default: IntEnum = None) -> dataclasses.Field: + """ + Field generator function for enum field + + :param IntEnum enumclass: Class with enums. + :param IntEnum default: default value + :return: dataclass field + """ return dataclasses.field(default=default, metadata={"enum": enumclass}) @dataclasses.dataclass class BinmapDataclass(ABC): + """ + Dataclass that does the converting to and from binary data + """ + _byteorder = "" _formatstring = "" - __binarydata: dataclasses.InitVar[bytes] = b"" + _binarydata: dataclasses.InitVar[bytes] = b"" def __init_subclass__(cls, byteorder: str = ">"): + """ + Subclass initiator. + :param str byteorder: byteorder for binary data + """ cls._byteorder = byteorder def __bytes__(self): + """ + Packs the class' fields to a binary string + :return: Binary string packed. + :rtype: bytes + """ return struct.pack( # TODO: use datclass.fields self._byteorder + self._formatstring, @@ -182,6 +228,10 @@ def __bytes__(self): ) def __post_init__(self, _binarydata: bytes): + """ + Initialises fields from a binary string + :param bytes _binarydata: Binary string that will be unpacked. + """ if _binarydata != b"": self._unpacker(_binarydata) # Kludgy hack to keep order @@ -196,6 +246,10 @@ def __post_init__(self, _binarydata: bytes): self.__dict__.update({f.name: val}) def _unpacker(self, value: bytes): + """ + Unpacks value to each field + :param bytes value: binary string to unpack + """ type_hints = get_type_hints(self) datafieldsmap = {f.name: f for f in dataclasses.fields(self)} datafields = [ @@ -210,5 +264,9 @@ def _unpacker(self, value: bytes): setattr(self, name, arg) def frombytes(self, value: bytes): + """ + Public method to convert from byte string + :param bytes value: binary string to unpack + """ self._unpacker(value) self._binarydata = value From 7d60a7336d9b8be93e0ae4980f764a05ffeac289 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Wed, 4 Mar 2020 22:42:43 +0100 Subject: [PATCH 10/18] Make black and isort be friends. --- tox.ini | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tox.ini b/tox.ini index 65d6dda..aa3ee94 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,12 @@ jobs = 1 max-line-length = 160 [isort] +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +line_length=88 + [tox] envlist = py37 From 41cffd405c85807b567146b677ea6fda65f96a65 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Wed, 4 Mar 2020 22:43:32 +0100 Subject: [PATCH 11/18] Put types in own file. --- binmap/__init__.py | 42 ++++++++++++++++++++++-------------------- binmap/types.py | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 20 deletions(-) create mode 100644 binmap/types.py diff --git a/binmap/__init__.py b/binmap/__init__.py index d9e7139..46bf549 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -2,7 +2,28 @@ import struct from abc import ABC from enum import IntEnum -from typing import Dict, NewType, Tuple, Type, TypeVar, Union, get_type_hints +from typing import Dict, Tuple, Type, TypeVar, Union, get_type_hints + +from binmap.types import ( + boolean, + char, + double, + floating, + halffloat, + integer, + long, + longlong, + pad, + pascalstring, + short, + signedchar, + string, + unsignedchar, + unsignedinteger, + unsignedlong, + unsignedlonglong, + unsignedshort, +) T = TypeVar("T") @@ -68,25 +89,6 @@ def __set__(self, obj, value): obj.__dict__.update({self.name: value}) -char = NewType("char", int) -signedchar = NewType("signedchar", int) -unsignedchar = NewType("unsignedchar", int) -boolean = NewType("boolean", bool) -short = NewType("short", int) -unsignedshort = NewType("unsignedshort", int) -integer = NewType("integer", int) -unsignedinteger = NewType("unsignedinteger", int) -long = NewType("long", int) -unsignedlong = NewType("unsignedlong", int) -longlong = NewType("longlong", int) -unsignedlonglong = NewType("unsignedlonglong", int) -halffloat = NewType("halffloat", float) -floating = NewType("floating", float) -double = NewType("double", float) -string = NewType("string", str) -pascalstring = NewType("pascalstring", str) -pad = NewType("pad", int) - datatypemapping: Dict[type, Tuple[Type[BaseDescriptor], str]] = { char: (BinField, "c"), signedchar: (BinField, "b"), diff --git a/binmap/types.py b/binmap/types.py new file mode 100644 index 0000000..4abdd29 --- /dev/null +++ b/binmap/types.py @@ -0,0 +1,20 @@ +from typing import NewType + +char = NewType("char", int) +signedchar = NewType("signedchar", int) +unsignedchar = NewType("unsignedchar", int) +boolean = NewType("boolean", bool) +short = NewType("short", int) +unsignedshort = NewType("unsignedshort", int) +integer = NewType("integer", int) +unsignedinteger = NewType("unsignedinteger", int) +long = NewType("long", int) +unsignedlong = NewType("unsignedlong", int) +longlong = NewType("longlong", int) +unsignedlonglong = NewType("unsignedlonglong", int) +halffloat = NewType("halffloat", float) +floating = NewType("floating", float) +double = NewType("double", float) +string = NewType("string", str) +pascalstring = NewType("pascalstring", str) +pad = NewType("pad", int) From bdb2df991f2388405fbf66e4d783017e47b14464 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Thu, 5 Mar 2020 12:26:15 +0100 Subject: [PATCH 12/18] Make README render Missed empty line --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 7768a0d..07685fc 100644 --- a/README.rst +++ b/README.rst @@ -5,6 +5,7 @@ It follows dataclass pattern with typehinting as the binary format. Temperature with one unsigned byte: .. code-block:: python + @binmapdataclass class Temperature(BinmapDataclass): temp: unsignedchar = 0 @@ -22,6 +23,7 @@ Temperature and humidity consisting of one signed byte for temperature and one unsiged byte for humidity: .. code-block:: python + @binmapdataclass class TempHum(BinmapDataclass): temp: signedchar = 0 From ca8a847e80a534e1e52068c3ecb2b0832095b180 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Fri, 6 Mar 2020 22:28:46 +0100 Subject: [PATCH 13/18] Make binmapdataclass an implicit dataclass. --- binmap/__init__.py | 57 +++++++++++++++++++------------------------- tests/test_binmap.py | 9 ------- 2 files changed, 24 insertions(+), 42 deletions(-) diff --git a/binmap/__init__.py b/binmap/__init__.py index 46bf549..cb6c5a9 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -115,38 +115,6 @@ def __set__(self, obj, value): } -def binmapdataclass(cls: Type[T]) -> Type[T]: - """ - Decorator to create dataclass for binmapping - - :param cls: Class to decorate - :return: Decorated class - """ - dataclasses.dataclass(cls) - type_hints = get_type_hints(cls) - - cls._formatstring = "" - - for field_ in dataclasses.fields(cls): - _base, _type = datatypemapping[type_hints[field_.name]] - if "constant" in field_.metadata: - _base = ConstField - elif "enum" in field_.metadata: - _base = EnumField - setattr(cls, field_.name, _base(name=field_.name)) - if type_hints[field_.name] is pad: - _type = field_.default * _type - if ( - type_hints[field_.name] is string - or type_hints[field_.name] is pascalstring - or type_hints[field_.name] is str - ): - _type = str(field_.metadata["length"]) + _type - cls._formatstring += _type - - return cls - - def padding(length: int = 1) -> dataclasses.Field: """ Field generator function for padding elements @@ -208,10 +176,33 @@ class BinmapDataclass(ABC): def __init_subclass__(cls, byteorder: str = ">"): """ - Subclass initiator. + Subclass initiator. This makes the inheriting class a dataclass. :param str byteorder: byteorder for binary data """ cls._byteorder = byteorder + dataclasses.dataclass(cls) + type_hints = get_type_hints(cls) + + cls._formatstring = "" + + for field_ in dataclasses.fields(cls): + _base, _type = datatypemapping[type_hints[field_.name]] + if "constant" in field_.metadata: + _base = ConstField + elif "enum" in field_.metadata: + _base = EnumField + setattr(cls, field_.name, _base(name=field_.name)) + if type_hints[field_.name] is pad: + _type = field_.default * _type + if ( + type_hints[field_.name] is string + or type_hints[field_.name] is pascalstring + or type_hints[field_.name] is str + ): + _type = str(field_.metadata["length"]) + _type + cls._formatstring += _type + + return cls def __bytes__(self): """ diff --git a/tests/test_binmap.py b/tests/test_binmap.py index 5bb8e59..c2c319e 100644 --- a/tests/test_binmap.py +++ b/tests/test_binmap.py @@ -18,12 +18,10 @@ def test_baseclass_with_keyword(): assert "got an unexpected keyword argument 'temp'" in str(excinfo) -@binmap.binmapdataclass class Temp(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 -@binmap.binmapdataclass class TempHum(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 humidity: binmap.unsignedchar = 0 @@ -36,12 +34,10 @@ def test_different_classes_eq(): assert t.temp == th.temp -@binmap.binmapdataclass class Bigendian(binmap.BinmapDataclass): value: binmap.longlong = 0 -@binmap.binmapdataclass class Littleedian(binmap.BinmapDataclass, byteorder="<"): value: binmap.longlong = 0 @@ -173,14 +169,12 @@ def test_compare_not_equal(self): assert th2 != th4 -@binmap.binmapdataclass class Pad(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 pad: binmap.pad = binmap.padding(2) humidity: binmap.unsignedchar = 0 -@binmap.binmapdataclass class AdvancedPad(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 _pad1: binmap.pad = binmap.padding(2) @@ -253,7 +247,6 @@ class WindEnum(IntEnum): West = 3 -@binmap.binmapdataclass class EnumClass(binmap.BinmapDataclass): temp: binmap.unsignedchar = 0 wind: binmap.unsignedchar = binmap.enumfield(WindEnum, default=WindEnum.East) @@ -288,7 +281,6 @@ def test_set_named_enum(self): assert "1.2 is not a valid WindEnum" in str(excinfo) -@binmap.binmapdataclass class ConstValues(binmap.BinmapDataclass): datatype: binmap.unsignedchar = binmap.constant(0x15) status: binmap.unsignedchar = 0 @@ -322,7 +314,6 @@ def test_binary_data(self): assert c.status == 1 -@binmap.binmapdataclass class AllDatatypes(binmap.BinmapDataclass): _pad: binmap.pad = binmap.padding(1) char: binmap.char = b"\x00" From 1d2d1092568662254a70e4a21d16e8a4694d4fc5 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Fri, 6 Mar 2020 22:44:52 +0100 Subject: [PATCH 14/18] Make types an explicit import. - Add shortnames for types that matches tha names in struct. --- binmap/__init__.py | 67 ++++++++++++++++------------------------- binmap/st.py | 20 +++++++++++++ tests/test_binmap.py | 71 ++++++++++++++++++++++---------------------- 3 files changed, 81 insertions(+), 77 deletions(-) create mode 100644 binmap/st.py diff --git a/binmap/__init__.py b/binmap/__init__.py index cb6c5a9..dce0b00 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -4,26 +4,7 @@ from enum import IntEnum from typing import Dict, Tuple, Type, TypeVar, Union, get_type_hints -from binmap.types import ( - boolean, - char, - double, - floating, - halffloat, - integer, - long, - longlong, - pad, - pascalstring, - short, - signedchar, - string, - unsignedchar, - unsignedinteger, - unsignedlong, - unsignedlonglong, - unsignedshort, -) +from binmap import types T = TypeVar("T") @@ -90,28 +71,28 @@ def __set__(self, obj, value): datatypemapping: Dict[type, Tuple[Type[BaseDescriptor], str]] = { - char: (BinField, "c"), - signedchar: (BinField, "b"), - unsignedchar: (BinField, "B"), - boolean: (BinField, "?"), + types.char: (BinField, "c"), + types.signedchar: (BinField, "b"), + types.unsignedchar: (BinField, "B"), + types.boolean: (BinField, "?"), bool: (BinField, "?"), - short: (BinField, "h"), - unsignedshort: (BinField, "H"), - integer: (BinField, "i"), + types.short: (BinField, "h"), + types.unsignedshort: (BinField, "H"), + types.integer: (BinField, "i"), int: (BinField, "i"), - unsignedinteger: (BinField, "I"), - long: (BinField, "l"), - unsignedlong: (BinField, "L"), - longlong: (BinField, "q"), - unsignedlonglong: (BinField, "Q"), - halffloat: (BinField, "e"), - floating: (BinField, "f"), + types.unsignedinteger: (BinField, "I"), + types.long: (BinField, "l"), + types.unsignedlong: (BinField, "L"), + types.longlong: (BinField, "q"), + types.unsignedlonglong: (BinField, "Q"), + types.halffloat: (BinField, "e"), + types.floating: (BinField, "f"), float: (BinField, "f"), - double: (BinField, "d"), - string: (BinField, "s"), + types.double: (BinField, "d"), + types.string: (BinField, "s"), str: (BinField, "s"), - pascalstring: (BinField, "p"), - pad: (PaddingField, "x"), + types.pascalstring: (BinField, "p"), + types.pad: (PaddingField, "x"), } @@ -192,11 +173,11 @@ def __init_subclass__(cls, byteorder: str = ">"): elif "enum" in field_.metadata: _base = EnumField setattr(cls, field_.name, _base(name=field_.name)) - if type_hints[field_.name] is pad: + if type_hints[field_.name] is types.pad: _type = field_.default * _type if ( - type_hints[field_.name] is string - or type_hints[field_.name] is pascalstring + type_hints[field_.name] is types.string + or type_hints[field_.name] is types.pascalstring or type_hints[field_.name] is str ): _type = str(field_.metadata["length"]) + _type @@ -246,7 +227,9 @@ def _unpacker(self, value: bytes): type_hints = get_type_hints(self) datafieldsmap = {f.name: f for f in dataclasses.fields(self)} datafields = [ - f.name for f in dataclasses.fields(self) if not (type_hints[f.name] is pad) + f.name + for f in dataclasses.fields(self) + if not (type_hints[f.name] is types.pad) ] args = struct.unpack(self._byteorder + self._formatstring, value) for arg, name in zip(args, datafields): diff --git a/binmap/st.py b/binmap/st.py new file mode 100644 index 0000000..70ab462 --- /dev/null +++ b/binmap/st.py @@ -0,0 +1,20 @@ +from binmap import types + +_b = types.boolean +c = types.char +d = types.double +f = types.floating +e = types.halffloat +i = types.integer +l = types.long # noqa: E741 +q = types.longlong +x = types.pad +p = types.pascalstring +h = types.short +b = types.signedchar +s = types.string +B = types.unsignedchar +I = types.unsignedinteger # noqa: E741 +L = types.unsignedlong +Q = types.unsignedlonglong +H = types.unsignedshort diff --git a/tests/test_binmap.py b/tests/test_binmap.py index c2c319e..4f16284 100644 --- a/tests/test_binmap.py +++ b/tests/test_binmap.py @@ -4,6 +4,7 @@ import pytest import binmap +from binmap import types def test_baseclass(): @@ -19,12 +20,12 @@ def test_baseclass_with_keyword(): class Temp(binmap.BinmapDataclass): - temp: binmap.unsignedchar = 0 + temp: types.unsignedchar = 0 class TempHum(binmap.BinmapDataclass): - temp: binmap.unsignedchar = 0 - humidity: binmap.unsignedchar = 0 + temp: types.unsignedchar = 0 + humidity: types.unsignedchar = 0 def test_different_classes_eq(): @@ -35,11 +36,11 @@ def test_different_classes_eq(): class Bigendian(binmap.BinmapDataclass): - value: binmap.longlong = 0 + value: types.longlong = 0 class Littleedian(binmap.BinmapDataclass, byteorder="<"): - value: binmap.longlong = 0 + value: types.longlong = 0 def test_dataformats(): @@ -170,17 +171,17 @@ def test_compare_not_equal(self): class Pad(binmap.BinmapDataclass): - temp: binmap.unsignedchar = 0 - pad: binmap.pad = binmap.padding(2) - humidity: binmap.unsignedchar = 0 + temp: types.unsignedchar = 0 + pad: types.pad = binmap.padding(2) + humidity: types.unsignedchar = 0 class AdvancedPad(binmap.BinmapDataclass): - temp: binmap.unsignedchar = 0 - _pad1: binmap.pad = binmap.padding(2) - humidity: binmap.unsignedchar = 0 - _pad2: binmap.pad = binmap.padding(3) - _pad3: binmap.pad = binmap.padding(1) + temp: types.unsignedchar = 0 + _pad1: types.pad = binmap.padding(2) + humidity: types.unsignedchar = 0 + _pad2: types.pad = binmap.padding(3) + _pad3: types.pad = binmap.padding(1) class TestPadClass: @@ -248,8 +249,8 @@ class WindEnum(IntEnum): class EnumClass(binmap.BinmapDataclass): - temp: binmap.unsignedchar = 0 - wind: binmap.unsignedchar = binmap.enumfield(WindEnum, default=WindEnum.East) + temp: types.unsignedchar = 0 + wind: types.unsignedchar = binmap.enumfield(WindEnum, default=WindEnum.East) class TestEnumClass: @@ -282,8 +283,8 @@ def test_set_named_enum(self): class ConstValues(binmap.BinmapDataclass): - datatype: binmap.unsignedchar = binmap.constant(0x15) - status: binmap.unsignedchar = 0 + datatype: types.unsignedchar = binmap.constant(0x15) + status: types.unsignedchar = 0 class TestConstValues: @@ -315,24 +316,24 @@ def test_binary_data(self): class AllDatatypes(binmap.BinmapDataclass): - _pad: binmap.pad = binmap.padding(1) - char: binmap.char = b"\x00" - signedchar: binmap.signedchar = 0 - unsignedchar: binmap.unsignedchar = 0 - boolean: binmap.boolean = False - short: binmap.short = 0 - unsignedshort: binmap.unsignedshort = 0 - integer: binmap.integer = 0 - unsignedint: binmap.unsignedinteger = 0 - long: binmap.long = 0 - unsignedlong: binmap.unsignedlong = 0 - longlong: binmap.longlong = 0 - unsignedlonglong: binmap.unsignedlonglong = 0 - halffloat: binmap.halffloat = 0.0 - floating: binmap.floating = 0.0 - double: binmap.double = 0.0 - string: binmap.string = binmap.stringfield(10) - pascalstring: binmap.pascalstring = binmap.stringfield(15) + _pad: types.pad = binmap.padding(1) + char: types.char = b"\x00" + signedchar: types.signedchar = 0 + unsignedchar: types.unsignedchar = 0 + boolean: types.boolean = False + short: types.short = 0 + unsignedshort: types.unsignedshort = 0 + integer: types.integer = 0 + unsignedint: types.unsignedinteger = 0 + long: types.long = 0 + unsignedlong: types.unsignedlong = 0 + longlong: types.longlong = 0 + unsignedlonglong: types.unsignedlonglong = 0 + halffloat: types.halffloat = 0.0 + floating: types.floating = 0.0 + double: types.double = 0.0 + string: types.string = binmap.stringfield(10) + pascalstring: types.pascalstring = binmap.stringfield(15) class TestAllDatatypes: From 38b856a133ec000686b47bab6b6ff971c50b8693 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sat, 7 Mar 2020 09:59:58 +0100 Subject: [PATCH 15/18] Removed unused attributes. --- binmap/__init__.py | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/binmap/__init__.py b/binmap/__init__.py index dce0b00..05ca4e5 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -151,7 +151,6 @@ class BinmapDataclass(ABC): Dataclass that does the converting to and from binary data """ - _byteorder = "" _formatstring = "" _binarydata: dataclasses.InitVar[bytes] = b"" @@ -160,11 +159,10 @@ def __init_subclass__(cls, byteorder: str = ">"): Subclass initiator. This makes the inheriting class a dataclass. :param str byteorder: byteorder for binary data """ - cls._byteorder = byteorder dataclasses.dataclass(cls) type_hints = get_type_hints(cls) - cls._formatstring = "" + cls._formatstring = byteorder for field_ in dataclasses.fields(cls): _base, _type = datatypemapping[type_hints[field_.name]] @@ -183,8 +181,6 @@ def __init_subclass__(cls, byteorder: str = ">"): _type = str(field_.metadata["length"]) + _type cls._formatstring += _type - return cls - def __bytes__(self): """ Packs the class' fields to a binary string @@ -193,12 +189,8 @@ def __bytes__(self): """ return struct.pack( # TODO: use datclass.fields - self._byteorder + self._formatstring, - *( - v - for k, v in self.__dict__.items() - if k not in ["_byteorder", "_formatstring", "_binarydata"] - ), + self._formatstring, + *(v for k, v in self.__dict__.items() if k not in ["_formatstring"]), ) def __post_init__(self, _binarydata: bytes): @@ -207,7 +199,7 @@ def __post_init__(self, _binarydata: bytes): :param bytes _binarydata: Binary string that will be unpacked. """ if _binarydata != b"": - self._unpacker(_binarydata) + self.frombytes(_binarydata) # Kludgy hack to keep order for f in dataclasses.fields(self): if "padding" in f.metadata: @@ -219,7 +211,7 @@ def __post_init__(self, _binarydata: bytes): del self.__dict__[f.name] self.__dict__.update({f.name: val}) - def _unpacker(self, value: bytes): + def frombytes(self, value: bytes): """ Unpacks value to each field :param bytes value: binary string to unpack @@ -231,18 +223,10 @@ def _unpacker(self, value: bytes): for f in dataclasses.fields(self) if not (type_hints[f.name] is types.pad) ] - args = struct.unpack(self._byteorder + self._formatstring, value) + args = struct.unpack(self._formatstring, value) for arg, name in zip(args, datafields): if "constant" in datafieldsmap[name].metadata: if arg != datafieldsmap[name].default: raise ValueError("Constant doesn't match binary data") setattr(self, name, arg) - - def frombytes(self, value: bytes): - """ - Public method to convert from byte string - :param bytes value: binary string to unpack - """ - self._unpacker(value) - self._binarydata = value From ea7d5e02f12fb551b805530ac205e2235f933a84 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sat, 7 Mar 2020 13:04:39 +0100 Subject: [PATCH 16/18] Remove ABC --- binmap/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/binmap/__init__.py b/binmap/__init__.py index 05ca4e5..0aebc10 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -1,6 +1,5 @@ import dataclasses import struct -from abc import ABC from enum import IntEnum from typing import Dict, Tuple, Type, TypeVar, Union, get_type_hints @@ -146,7 +145,7 @@ def enumfield(enumclass: IntEnum, default: IntEnum = None) -> dataclasses.Field: @dataclasses.dataclass -class BinmapDataclass(ABC): +class BinmapDataclass: """ Dataclass that does the converting to and from binary data """ From 761f58792b3d4668a719c1ff0a0f114293ff88ad Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Mon, 9 Mar 2020 08:58:03 +0100 Subject: [PATCH 17/18] Only need inheritance No need for decorator now. --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index 07685fc..085277a 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,6 @@ Temperature with one unsigned byte: .. code-block:: python - @binmapdataclass class Temperature(BinmapDataclass): temp: unsignedchar = 0 @@ -24,7 +23,6 @@ one unsiged byte for humidity: .. code-block:: python - @binmapdataclass class TempHum(BinmapDataclass): temp: signedchar = 0 hum: unsignedchar = 0 From f138a5371c65a6203861804fbde3f26bd59438f5 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Mon, 9 Mar 2020 18:30:37 +0100 Subject: [PATCH 18/18] Simplified and moved some intialisations. --- binmap/__init__.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/binmap/__init__.py b/binmap/__init__.py index 0aebc10..6fdd720 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -66,7 +66,7 @@ def __set__(self, obj, value): if self.name in obj.__dict__: raise AttributeError(f"{self.name} is a constant") else: - obj.__dict__.update({self.name: value}) + obj.__dict__[self.name] = value datatypemapping: Dict[type, Tuple[Type[BaseDescriptor], str]] = { @@ -161,6 +161,8 @@ def __init_subclass__(cls, byteorder: str = ">"): dataclasses.dataclass(cls) type_hints = get_type_hints(cls) + cls._datafields = [] + cls._datafieldsmap = {} cls._formatstring = byteorder for field_ in dataclasses.fields(cls): @@ -172,11 +174,7 @@ def __init_subclass__(cls, byteorder: str = ">"): setattr(cls, field_.name, _base(name=field_.name)) if type_hints[field_.name] is types.pad: _type = field_.default * _type - if ( - type_hints[field_.name] is types.string - or type_hints[field_.name] is types.pascalstring - or type_hints[field_.name] is str - ): + if type_hints[field_.name] in (types.string, types.pascalstring, str): _type = str(field_.metadata["length"]) + _type cls._formatstring += _type @@ -201,6 +199,7 @@ def __post_init__(self, _binarydata: bytes): self.frombytes(_binarydata) # Kludgy hack to keep order for f in dataclasses.fields(self): + self._datafieldsmap.update({f.name: f}) if "padding" in f.metadata: continue if "constant" in f.metadata: @@ -209,23 +208,17 @@ def __post_init__(self, _binarydata: bytes): val = getattr(self, f.name) del self.__dict__[f.name] self.__dict__.update({f.name: val}) + self._datafields.append(f.name) def frombytes(self, value: bytes): """ Unpacks value to each field :param bytes value: binary string to unpack """ - type_hints = get_type_hints(self) - datafieldsmap = {f.name: f for f in dataclasses.fields(self)} - datafields = [ - f.name - for f in dataclasses.fields(self) - if not (type_hints[f.name] is types.pad) - ] args = struct.unpack(self._formatstring, value) - for arg, name in zip(args, datafields): - if "constant" in datafieldsmap[name].metadata: - if arg != datafieldsmap[name].default: + for arg, name in zip(args, self._datafields): + if "constant" in self._datafieldsmap[name].metadata: + if arg != self._datafieldsmap[name].default: raise ValueError("Constant doesn't match binary data") setattr(self, name, arg)