From 3c57ba6e5e8af43c2e30dea7458f7b8792ec90eb Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sun, 8 Nov 2020 16:20:13 +0100 Subject: [PATCH] Add support to put calculated field allways last. --- binmap/__init__.py | 30 +++++++++++++++------ tests/test_binmap.py | 63 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/binmap/__init__.py b/binmap/__init__.py index 908f1b1..c8c98c7 100644 --- a/binmap/__init__.py +++ b/binmap/__init__.py @@ -178,14 +178,14 @@ def enumfield( return dataclasses.field(default=default, metadata={"enum": enumclass}) +def calculatedfield(function: Callable, last=False) -> dataclasses.Field: """ Field generator function for calculated fields :param Callable function: function that calculates the field. :return: dataclass field """ -def calculatedfield(function: Callable) -> dataclasses.Field: - return dataclasses.field(default=0, metadata={"function": function}) + return dataclasses.field(default=0, metadata={"function": function, "last": last}) @dataclasses.dataclass @@ -213,6 +213,7 @@ def __init_subclass__(cls, byteorder: str = ">"): cls.__formatstring = byteorder + lastfield = "" for field_ in dataclasses.fields(cls): if field_.name.startswith("_BinmapDataclass__"): continue @@ -230,7 +231,13 @@ def __init_subclass__(cls, byteorder: str = ">"): _type = field_.default * _type if type_hints[field_.name] in (types.string, types.pascalstring, str): _type = str(field_.metadata["length"]) + _type - cls.__formatstring += _type + if "last" in field_.metadata and field_.metadata["last"]: + if lastfield != "": + raise ValueError("Can't have more than one last") + lastfield = _type + else: + cls.__formatstring += _type + cls.__formatstring += lastfield def __bytes__(self): """ @@ -239,12 +246,19 @@ def __bytes__(self): :rtype: bytes """ values = [] + lastvalue = None for k, v in self.__dict__.items(): - if not k.startswith("_BinmapDataclass__"): - if callable(v): - values.append(v(self)) - else: - values.append(v) + if k.startswith("_BinmapDataclass__"): + continue + if callable(v): + v = v(self) + if "last" in self.__datafieldsmap[k].metadata and self.__datafieldsmap[k].metadata["last"]: + lastvalue = v + continue + values.append(v) + if lastvalue is not None: + values.append(lastvalue) + return struct.pack( self.__formatstring, *values, diff --git a/tests/test_binmap.py b/tests/test_binmap.py index 0d3e46e..b77fdc6 100644 --- a/tests/test_binmap.py +++ b/tests/test_binmap.py @@ -602,6 +602,21 @@ def chk(self) -> types.unsignedchar: checksum: types.unsignedchar = binmap.calculatedfield(chk) +class CalculatedFieldLast(binmap.BinmapDataclass): + temp: types.signedchar = 0 + + def chk_last(self): + checksum = 0 + for k, v in self.__dict__.items(): + if k.startswith("_") or callable(v): + continue + checksum += v + return checksum & 0xFF + + checksum: types.unsignedchar = binmap.calculatedfield(chk_last, last=True) + hum: types.unsignedchar = 0 + + class TestCalculatedField: def test_calculated_field(self): cf = CalculatedField() @@ -620,3 +635,51 @@ def test_calculated_field_binary(self): with pytest.raises(ValueError) as excinfo: CalculatedField(b"\xe4\x18\x00") assert "Wrong calculated value" in str(excinfo) + + def test_calculated_field_last(self): + + cfl = CalculatedFieldLast() + cfl.temp = 10 + cfl.hum = 20 + + assert cfl.checksum == 30 + assert bytes(cfl) == b"\x0a\x14\x1e" + + def test_calculated_field_last_inherit(self): + class CalculatedFieldLastInherit(CalculatedFieldLast): + lux: types.unsignedinteger = 0 + + cfli = CalculatedFieldLastInherit() + cfli.temp = 10 + cfli.hum = 20 + cfli.lux = 401 + assert bytes(cfli) == b"\x0a\x14\x00\x00\x01\x91\xaf" + + with pytest.raises(ValueError) as excinfo: + CalculatedFieldLastInherit(b"\x0b\x20\x00\x00\x01\x30\x00") + assert "Wrong calculated value" in str(excinfo) + + def test_calculated_field_multi_last(self): + with pytest.raises(ValueError) as excinfo: + + class CalculatedFieldMultiLast(binmap.BinmapDataclass): + temp: types.unsignedchar = 0 + + def chk(self): + return 0 + + checksum1: types.unsignedchar = binmap.calculatedfield(chk, last=True) + checksum2: types.unsignedchar = binmap.calculatedfield(chk, last=True) + + assert "Can't have more than one last" in str(excinfo) + + def test_calculated_field_multi_last_inherit(self): + with pytest.raises(ValueError) as excinfo: + + class CalculatedFieldMultiLastInherit(CalculatedFieldLast): + def chk2(self): + return 0 + + checksum2: types.unsignedchar = binmap.calculatedfield(chk2, last=True) + + assert "Can't have more than one last" in str(excinfo)