structo 0.0.4__tar.gz → 0.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {structo-0.0.4 → structo-0.0.6}/PKG-INFO +1 -1
- {structo-0.0.4 → structo-0.0.6}/structo/__init__.py +5 -3
- {structo-0.0.4 → structo-0.0.6}/structo/object.py +2 -3
- structo-0.0.6/structo/packed.py +78 -0
- {structo-0.0.4 → structo-0.0.6}/structo/serialise.py +3 -8
- {structo-0.0.4 → structo-0.0.6}/structo/serializer.py +6 -0
- {structo-0.0.4 → structo-0.0.6}/structo/types/__init__.py +1 -0
- {structo-0.0.4 → structo-0.0.6}/structo/types/list.py +18 -3
- structo-0.0.6/structo/types/packed.py +59 -0
- structo-0.0.6/tests/test_object.py +54 -0
- structo-0.0.6/tests/test_packed.py +95 -0
- structo-0.0.4/union.py +0 -8
- {structo-0.0.4 → structo-0.0.6}/.gitignore +0 -0
- {structo-0.0.4 → structo-0.0.6}/LICENSE +0 -0
- {structo-0.0.4 → structo-0.0.6}/README.md +0 -0
- {structo-0.0.4 → structo-0.0.6}/examples/custom_serializer.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/examples/iterable_serializer.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/examples/wav.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/pyproject.toml +0 -0
- {structo-0.0.4 → structo-0.0.6}/pytest.ini +0 -0
- {structo-0.0.4 → structo-0.0.6}/structo/types/array.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/structo/types/blob.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/structo/types/buffer.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/structo/types/literal.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/structo/types/object.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/structo/types/primatives.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/structo/types/string.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/structo/utils.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/tests/test_array.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/tests/test_primatives.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/tests/test_size.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/tests/utils.py +0 -0
- {structo-0.0.4 → structo-0.0.6}/uv.lock +0 -0
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
Structify
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
__version__ = "0.0.
|
|
5
|
+
__version__ = "0.0.6"
|
|
6
6
|
|
|
7
|
-
from .serializer import Serializer
|
|
7
|
+
from .serializer import Serializer, Serialiable
|
|
8
|
+
from .serialise import get_serializer
|
|
9
|
+
from .object import SerializableObject
|
|
10
|
+
from .packed import PackedInts, PackedInt
|
|
8
11
|
from .types import (
|
|
9
12
|
uint64_BE,
|
|
10
13
|
uint64_LE,
|
|
@@ -40,7 +43,6 @@ from .types import (
|
|
|
40
43
|
Literal,
|
|
41
44
|
ObjectSerializer,
|
|
42
45
|
)
|
|
43
|
-
from .object import SerializableObject
|
|
44
46
|
from .utils import (
|
|
45
47
|
StructoReader,
|
|
46
48
|
StructifyWriter,
|
|
@@ -3,7 +3,7 @@ import annotationlib
|
|
|
3
3
|
import typing as t
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
|
|
6
|
-
from .serializer import Serializer
|
|
6
|
+
from .serializer import Serialiable, Serializer
|
|
7
7
|
from .serialise import get_serializer
|
|
8
8
|
|
|
9
9
|
|
|
@@ -32,11 +32,10 @@ class SerializableObjectMeta(type):
|
|
|
32
32
|
|
|
33
33
|
# This is very messed up since we
|
|
34
34
|
@t.dataclass_transform()
|
|
35
|
-
class SerializableObject(metaclass=SerializableObjectMeta):
|
|
35
|
+
class SerializableObject(Serialiable, metaclass=SerializableObjectMeta):
|
|
36
36
|
@classmethod
|
|
37
37
|
def serializer(cls) -> Serializer[t.Self]:
|
|
38
38
|
from .types import ObjectSerializer
|
|
39
|
-
|
|
40
39
|
return ObjectSerializer(cls)
|
|
41
40
|
|
|
42
41
|
@classmethod
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from .serializer import Serialiable, Serializer
|
|
2
|
+
import typing as t
|
|
3
|
+
import io
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import annotationlib
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PackedInt:
|
|
10
|
+
bits: int
|
|
11
|
+
|
|
12
|
+
def __init__(self, *, bits: int) -> None:
|
|
13
|
+
self.bits = bits
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PackedIntsMeta(type):
|
|
17
|
+
def __new__(cls, name, bases, dct):
|
|
18
|
+
new_class = t.cast(type, super().__new__(cls, name, bases, dct))
|
|
19
|
+
if name == "PackedInts":
|
|
20
|
+
return new_class
|
|
21
|
+
|
|
22
|
+
annotate = annotationlib.get_annotate_from_class_namespace(dct)
|
|
23
|
+
assert annotate
|
|
24
|
+
|
|
25
|
+
bits: dict[str, PackedInt] = {}
|
|
26
|
+
attrs = annotate(annotationlib.Format.VALUE_WITH_FAKE_GLOBALS)
|
|
27
|
+
for key, value in attrs.items():
|
|
28
|
+
assert t.get_origin(value) is t.Annotated
|
|
29
|
+
|
|
30
|
+
ints = [arg for arg in t.get_args(value) if isinstance(arg, PackedInt)]
|
|
31
|
+
assert len(ints) == 1
|
|
32
|
+
bits[key] = ints[0]
|
|
33
|
+
|
|
34
|
+
obj = t.cast(PackedInts, dataclass(new_class))
|
|
35
|
+
obj._bits = bits
|
|
36
|
+
return obj
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# This is very messed up since we
|
|
40
|
+
@t.dataclass_transform()
|
|
41
|
+
class PackedInts(Serialiable, metaclass=PackedIntsMeta):
|
|
42
|
+
_bits: dict[str, PackedInt]
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def serializer(cls) -> Serializer[t.Self]:
|
|
46
|
+
from .types import PackedIntSerializer
|
|
47
|
+
|
|
48
|
+
return PackedIntSerializer(cls)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def sizeof(cls) -> int | None:
|
|
52
|
+
return cls.serializer().sizeof()
|
|
53
|
+
|
|
54
|
+
def write(self, buf: io.Writer):
|
|
55
|
+
return self.serializer().write(buf, self)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def read(cls, buf: io.Reader) -> t.Self:
|
|
59
|
+
return cls.serializer().read(buf)
|
|
60
|
+
|
|
61
|
+
def to_bytes(self) -> bytes:
|
|
62
|
+
return self.serializer().to_bytes(self)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_bytes(cls, data: bytes) -> t.Self:
|
|
66
|
+
return cls.serializer().from_bytes(data)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Foo(PackedInts):
|
|
70
|
+
type: Annotated[int, PackedInt(bits=2)]
|
|
71
|
+
bar: Annotated[int, PackedInt(bits=10)]
|
|
72
|
+
offset: Annotated[int, PackedInt(bits=4)]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
obj = Foo(type=1, bar=2, offset=3)
|
|
76
|
+
|
|
77
|
+
print(obj)
|
|
78
|
+
print(Foo.from_bytes(obj.to_bytes()))
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import io
|
|
2
1
|
import typing as t
|
|
3
|
-
from .serializer import Serializer
|
|
2
|
+
from .serializer import Serializer, Serialiable
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
def get_serializer(format: type) -> Serializer:
|
|
7
|
-
from .serializer import Serializer
|
|
8
|
-
from .object import SerializableObject
|
|
9
|
-
from .types.object import ObjectSerializer
|
|
10
|
-
|
|
11
6
|
if t.get_origin(format) is t.Annotated:
|
|
12
7
|
args = t.get_args(format)
|
|
13
8
|
serializers = [arg for arg in args if isinstance(arg, Serializer)]
|
|
@@ -17,8 +12,8 @@ def get_serializer(format: type) -> Serializer:
|
|
|
17
12
|
|
|
18
13
|
return serializer
|
|
19
14
|
|
|
20
|
-
if issubclass(format,
|
|
21
|
-
return
|
|
15
|
+
if issubclass(format, Serialiable):
|
|
16
|
+
return format.serializer()
|
|
22
17
|
|
|
23
18
|
# Nicely formatted errors:
|
|
24
19
|
if format == int:
|
|
@@ -1,15 +1,30 @@
|
|
|
1
|
+
import typing as t
|
|
1
2
|
from ..serializer import Serializer
|
|
3
|
+
from ..object import SerializableObject
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
class List[T](Serializer[list[T]]):
|
|
5
7
|
"A list of items, prefixed with it's length"
|
|
6
8
|
|
|
7
|
-
value_type: Serializer[T]
|
|
8
9
|
length_type: Serializer[int]
|
|
10
|
+
value_type: Serializer
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
@t.overload
|
|
13
|
+
def __init__(
|
|
14
|
+
self, length_type: Serializer[int], value_type: Serializer[T]
|
|
15
|
+
) -> None: ...
|
|
16
|
+
|
|
17
|
+
@t.overload
|
|
18
|
+
def __init__(
|
|
19
|
+
self, length_type: Serializer[int], value_type: T
|
|
20
|
+
) -> None: ...
|
|
21
|
+
|
|
22
|
+
def __init__(self, length_type: Serializer[int], value_type: Serializer | SerializableObject) -> None:
|
|
11
23
|
self.length_type = length_type
|
|
12
|
-
|
|
24
|
+
if isinstance(value_type, SerializableObject):
|
|
25
|
+
self.value_type = value_type.serializer()
|
|
26
|
+
else:
|
|
27
|
+
self.value_type = value_type
|
|
13
28
|
|
|
14
29
|
def write(self, buf, value):
|
|
15
30
|
length = len(value)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from io import Reader, Writer
|
|
2
|
+
from math import ceil
|
|
3
|
+
|
|
4
|
+
from structo import Serializer
|
|
5
|
+
from ..packed import PackedInts, PackedInt
|
|
6
|
+
import typing as t
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PackedIntSerializer[T: PackedInts](Serializer[T]):
|
|
10
|
+
size: int
|
|
11
|
+
_bits: dict[str, int]
|
|
12
|
+
_cls: type[type[T]]
|
|
13
|
+
|
|
14
|
+
def __init__(self, cls: type[T]) -> None:
|
|
15
|
+
total_bits = sum(x.bits for x in cls._bits.values())
|
|
16
|
+
self._cls = cls
|
|
17
|
+
self.size = ceil(total_bits / 8)
|
|
18
|
+
|
|
19
|
+
self._bits = {}
|
|
20
|
+
for key, value in cls._bits.items():
|
|
21
|
+
self._bits[key] = value.bits
|
|
22
|
+
|
|
23
|
+
def write(self, buf: Writer, value: T):
|
|
24
|
+
output = 0
|
|
25
|
+
offset = 0
|
|
26
|
+
for field_key, bits in self._bits.items():
|
|
27
|
+
field_value = getattr(value, field_key)
|
|
28
|
+
|
|
29
|
+
max_value = (2**bits) - 1
|
|
30
|
+
assert field_value <= max_value, f"{self._cls.__name__}{field_key} exceed max value, {field_value} > {max_value}"
|
|
31
|
+
output += field_value << offset
|
|
32
|
+
offset += bits
|
|
33
|
+
|
|
34
|
+
data = bytearray(self.size)
|
|
35
|
+
for x in range(self.size):
|
|
36
|
+
byte = output & 255
|
|
37
|
+
output >>= 8
|
|
38
|
+
data[x] = byte
|
|
39
|
+
|
|
40
|
+
buf.write(data)
|
|
41
|
+
|
|
42
|
+
def read(self, buf: Reader) -> T:
|
|
43
|
+
data = buf.read(self.size)
|
|
44
|
+
integer = int.from_bytes(data, "little")
|
|
45
|
+
attrs: dict[str, t.Any] = {}
|
|
46
|
+
|
|
47
|
+
offset = 0
|
|
48
|
+
for field_key, bits in self._bits.items():
|
|
49
|
+
|
|
50
|
+
mask = (2**bits) - 1
|
|
51
|
+
value = (integer >> offset) & mask
|
|
52
|
+
|
|
53
|
+
offset += bits
|
|
54
|
+
attrs[field_key] = value
|
|
55
|
+
|
|
56
|
+
return self._cls(**attrs)
|
|
57
|
+
|
|
58
|
+
def sizeof(self) -> int:
|
|
59
|
+
return self.size
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from utils import test
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
import structo as st
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@test("SerialiableObject: object")
|
|
8
|
+
def _():
|
|
9
|
+
class Foo(st.SerializableObject):
|
|
10
|
+
a: t.Annotated[int, st.uint8]
|
|
11
|
+
|
|
12
|
+
obj = Foo(a=1)
|
|
13
|
+
assert obj == Foo.from_bytes(obj.to_bytes())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@test("SerialiableObject: multiple attributes")
|
|
17
|
+
def _():
|
|
18
|
+
class Foo(st.SerializableObject):
|
|
19
|
+
a: t.Annotated[int, st.uint8]
|
|
20
|
+
b: t.Annotated[int, st.uint16_BE]
|
|
21
|
+
|
|
22
|
+
obj = Foo(a=1, b=256)
|
|
23
|
+
assert obj == Foo.from_bytes(obj.to_bytes())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@test("SerialiableObject: sizeof")
|
|
27
|
+
def _():
|
|
28
|
+
|
|
29
|
+
class Foo(st.SerializableObject):
|
|
30
|
+
a: t.Annotated[int, st.uint8]
|
|
31
|
+
b: t.Annotated[str, st.uint8]
|
|
32
|
+
|
|
33
|
+
assert Foo.sizeof() == 2
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@test("SerialiableObject: sizeof unknowable")
|
|
37
|
+
def _():
|
|
38
|
+
class Foo(st.SerializableObject):
|
|
39
|
+
a: t.Annotated[int, st.uint8]
|
|
40
|
+
b: t.Annotated[str, st.String(st.uint8)]
|
|
41
|
+
|
|
42
|
+
assert Foo.sizeof() is None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@test("SerialiableObject: nested object")
|
|
46
|
+
def _():
|
|
47
|
+
class Foo(st.SerializableObject):
|
|
48
|
+
a: t.Annotated[int, st.uint8]
|
|
49
|
+
|
|
50
|
+
class Bar(st.SerializableObject):
|
|
51
|
+
foo: Foo
|
|
52
|
+
|
|
53
|
+
obj = Bar(foo=Foo(1))
|
|
54
|
+
assert obj == Bar.from_bytes(obj.to_bytes())
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from utils import test
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
import structo as st
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@test("PackedInts: serialise on single bytes")
|
|
9
|
+
def _():
|
|
10
|
+
class Foo(st.PackedInts):
|
|
11
|
+
a: t.Annotated[int, st.PackedInt(bits=2)]
|
|
12
|
+
b: t.Annotated[int, st.PackedInt(bits=3)]
|
|
13
|
+
|
|
14
|
+
obj = Foo(a=0, b=3)
|
|
15
|
+
assert obj == Foo.from_bytes(obj.to_bytes())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@test("PackedInts: serialise across byte boundaries single bytes")
|
|
19
|
+
def _():
|
|
20
|
+
class Foo(st.PackedInts):
|
|
21
|
+
a: t.Annotated[int, st.PackedInt(bits=5)]
|
|
22
|
+
b: t.Annotated[int, st.PackedInt(bits=6)]
|
|
23
|
+
c: t.Annotated[int, st.PackedInt(bits=5)]
|
|
24
|
+
|
|
25
|
+
obj = Foo(a=0, b=63, c=0)
|
|
26
|
+
assert obj == Foo.from_bytes(obj.to_bytes())
|
|
27
|
+
|
|
28
|
+
@test("PackedInts: multibyte integer")
|
|
29
|
+
def _():
|
|
30
|
+
class Foo(st.PackedInts):
|
|
31
|
+
a: t.Annotated[int, st.PackedInt(bits=11)]
|
|
32
|
+
b: t.Annotated[int, st.PackedInt(bits=5)]
|
|
33
|
+
|
|
34
|
+
obj = Foo(a=(2 ** 11) - 1, b=0)
|
|
35
|
+
assert obj == Foo.from_bytes(obj.to_bytes())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@test("PackedInts: max value throw error")
|
|
39
|
+
def _():
|
|
40
|
+
class Foo(st.PackedInts):
|
|
41
|
+
a: t.Annotated[int, st.PackedInt(bits=2)]
|
|
42
|
+
|
|
43
|
+
with pytest.raises(Exception):
|
|
44
|
+
Foo(a=4).to_bytes()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@test("PackedInts: max value throw error on constructor")
|
|
48
|
+
@pytest.mark.skip()
|
|
49
|
+
def _():
|
|
50
|
+
class Foo(st.PackedInts):
|
|
51
|
+
a: t.Annotated[int, st.PackedInt(bits=2)]
|
|
52
|
+
|
|
53
|
+
with pytest.raises(Exception):
|
|
54
|
+
Foo(a=4)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@test("PackedInts: sizeof")
|
|
58
|
+
def _():
|
|
59
|
+
class OneByte_A(st.PackedInts):
|
|
60
|
+
a: t.Annotated[int, st.PackedInt(bits=2)]
|
|
61
|
+
b: t.Annotated[int, st.PackedInt(bits=1)]
|
|
62
|
+
|
|
63
|
+
assert OneByte_A.sizeof() == 1
|
|
64
|
+
|
|
65
|
+
class OneByte_B(st.PackedInts):
|
|
66
|
+
a: t.Annotated[int, st.PackedInt(bits=8)]
|
|
67
|
+
|
|
68
|
+
assert OneByte_B.sizeof() == 1
|
|
69
|
+
|
|
70
|
+
class OneByte_C(st.PackedInts):
|
|
71
|
+
a: t.Annotated[int, st.PackedInt(bits=3)]
|
|
72
|
+
b: t.Annotated[int, st.PackedInt(bits=3)]
|
|
73
|
+
c: t.Annotated[int, st.PackedInt(bits=2)]
|
|
74
|
+
|
|
75
|
+
assert OneByte_C.sizeof() == 1
|
|
76
|
+
|
|
77
|
+
class TwoBytes_A(st.PackedInts):
|
|
78
|
+
a: t.Annotated[int, st.PackedInt(bits=5)]
|
|
79
|
+
b: t.Annotated[int, st.PackedInt(bits=4)]
|
|
80
|
+
|
|
81
|
+
assert TwoBytes_A.sizeof() == 2
|
|
82
|
+
|
|
83
|
+
class TwoBytes_B(st.PackedInts):
|
|
84
|
+
a: t.Annotated[int, st.PackedInt(bits=6)]
|
|
85
|
+
b: t.Annotated[int, st.PackedInt(bits=6)]
|
|
86
|
+
c: t.Annotated[int, st.PackedInt(bits=4)]
|
|
87
|
+
|
|
88
|
+
assert TwoBytes_B.sizeof() == 2
|
|
89
|
+
|
|
90
|
+
class ThreeBytes_A(st.PackedInts):
|
|
91
|
+
a: t.Annotated[int, st.PackedInt(bits=6)]
|
|
92
|
+
b: t.Annotated[int, st.PackedInt(bits=6)]
|
|
93
|
+
c: t.Annotated[int, st.PackedInt(bits=5)]
|
|
94
|
+
|
|
95
|
+
assert ThreeBytes_A.sizeof() == 3
|
structo-0.0.4/union.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|