structo 0.0.4__tar.gz → 0.0.5__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.
Files changed (32) hide show
  1. {structo-0.0.4 → structo-0.0.5}/PKG-INFO +1 -1
  2. {structo-0.0.4 → structo-0.0.5}/structo/__init__.py +5 -3
  3. {structo-0.0.4 → structo-0.0.5}/structo/object.py +2 -3
  4. structo-0.0.5/structo/packed.py +78 -0
  5. {structo-0.0.4 → structo-0.0.5}/structo/serialise.py +3 -8
  6. {structo-0.0.4 → structo-0.0.5}/structo/serializer.py +6 -0
  7. {structo-0.0.4 → structo-0.0.5}/structo/types/__init__.py +1 -0
  8. {structo-0.0.4 → structo-0.0.5}/structo/types/list.py +18 -3
  9. structo-0.0.5/structo/types/packed.py +59 -0
  10. structo-0.0.5/tests/test_packed.py +95 -0
  11. structo-0.0.4/union.py +0 -8
  12. {structo-0.0.4 → structo-0.0.5}/.gitignore +0 -0
  13. {structo-0.0.4 → structo-0.0.5}/LICENSE +0 -0
  14. {structo-0.0.4 → structo-0.0.5}/README.md +0 -0
  15. {structo-0.0.4 → structo-0.0.5}/examples/custom_serializer.py +0 -0
  16. {structo-0.0.4 → structo-0.0.5}/examples/iterable_serializer.py +0 -0
  17. {structo-0.0.4 → structo-0.0.5}/examples/wav.py +0 -0
  18. {structo-0.0.4 → structo-0.0.5}/pyproject.toml +0 -0
  19. {structo-0.0.4 → structo-0.0.5}/pytest.ini +0 -0
  20. {structo-0.0.4 → structo-0.0.5}/structo/types/array.py +0 -0
  21. {structo-0.0.4 → structo-0.0.5}/structo/types/blob.py +0 -0
  22. {structo-0.0.4 → structo-0.0.5}/structo/types/buffer.py +0 -0
  23. {structo-0.0.4 → structo-0.0.5}/structo/types/literal.py +0 -0
  24. {structo-0.0.4 → structo-0.0.5}/structo/types/object.py +0 -0
  25. {structo-0.0.4 → structo-0.0.5}/structo/types/primatives.py +0 -0
  26. {structo-0.0.4 → structo-0.0.5}/structo/types/string.py +0 -0
  27. {structo-0.0.4 → structo-0.0.5}/structo/utils.py +0 -0
  28. {structo-0.0.4 → structo-0.0.5}/tests/test_array.py +0 -0
  29. {structo-0.0.4 → structo-0.0.5}/tests/test_primatives.py +0 -0
  30. {structo-0.0.4 → structo-0.0.5}/tests/test_size.py +0 -0
  31. {structo-0.0.4 → structo-0.0.5}/tests/utils.py +0 -0
  32. {structo-0.0.4 → structo-0.0.5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structo
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Structify
5
5
  Author-email: Ben Brady <benbradybusiness@gmail.com>
6
6
  License-Expression: MIT
@@ -2,9 +2,12 @@
2
2
  Structify
3
3
  """
4
4
 
5
- __version__ = "0.0.4"
5
+ __version__ = "0.0.5"
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, SerializableObject):
21
- return ObjectSerializer(format)
15
+ if isinstance(format, Serialiable):
16
+ return format.serializer()
22
17
 
23
18
  # Nicely formatted errors:
24
19
  if format == int:
@@ -1,4 +1,10 @@
1
1
  import io
2
+ import typing as t
3
+
4
+
5
+ class Serialiable:
6
+ @classmethod
7
+ def serializer(cls) -> Serializer[t.Self]: ...
2
8
 
3
9
 
4
10
  class Serializer[T]:
@@ -33,3 +33,4 @@ from .list import List
33
33
  from .string import String
34
34
  from .literal import Literal
35
35
  from .object import ObjectSerializer
36
+ from .packed import PackedIntSerializer
@@ -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
- def __init__(self, length_type: Serializer[int], value_type: Serializer[T]) -> None:
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
- self.value_type = value_type
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,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
@@ -1,8 +0,0 @@
1
- from structo import Literal, serialize_to_bytes, SerializableObject
2
-
3
-
4
- class Foo(SerializableObject):
5
- bar: Literal[b"foo"] = ... # type: ignore
6
-
7
-
8
- serialize_to_bytes(Literal[b"foo"], b"foo")
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