firstclass 0.0.2__tar.gz → 0.0.3__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.
- {firstclass-0.0.2 → firstclass-0.0.3}/PKG-INFO +44 -2
- {firstclass-0.0.2 → firstclass-0.0.3}/README.md +43 -1
- {firstclass-0.0.2 → firstclass-0.0.3}/firstclass/__init__.py +7 -2
- {firstclass-0.0.2 → firstclass-0.0.3}/firstclass/_core.py +100 -25
- firstclass-0.0.3/firstclass/_header.py +1078 -0
- {firstclass-0.0.2 → firstclass-0.0.3}/firstclass/_specs.py +163 -1
- {firstclass-0.0.2 → firstclass-0.0.3}/firstclass.egg-info/PKG-INFO +44 -2
- {firstclass-0.0.2 → firstclass-0.0.3}/firstclass.egg-info/SOURCES.txt +6 -1
- {firstclass-0.0.2 → firstclass-0.0.3}/pyproject.toml +1 -1
- firstclass-0.0.3/tests/test_edge_cases.py +559 -0
- firstclass-0.0.3/tests/test_enum_bitfield.py +233 -0
- {firstclass-0.0.2 → firstclass-0.0.3}/tests/test_example.py +4 -0
- {firstclass-0.0.2 → firstclass-0.0.3}/tests/test_firstclass.py +39 -0
- firstclass-0.0.3/tests/test_header.py +582 -0
- firstclass-0.0.3/tests/test_header_parser.py +532 -0
- {firstclass-0.0.2 → firstclass-0.0.3}/firstclass.egg-info/dependency_links.txt +0 -0
- {firstclass-0.0.2 → firstclass-0.0.3}/firstclass.egg-info/top_level.txt +0 -0
- {firstclass-0.0.2 → firstclass-0.0.3}/setup.cfg +0 -0
- {firstclass-0.0.2 → firstclass-0.0.3}/tests/test_ctypes_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firstclass
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: dataclass with bytes, ctypes, C struct, JSON and XML serialization
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -47,11 +47,24 @@ Packet.sizeof() # 36
|
|
|
47
47
|
| `char_array[N]` | `char x[N]` | N (UTF-8, NUL-padded) |
|
|
48
48
|
| `bytes_t[N]` | `uint8_t x[N]` | N |
|
|
49
49
|
| `array_t[T, N]` | `T x[N]` | N * size(T) |
|
|
50
|
+
| `enum_t[Color, uint8_t]` | `uint8_t x` (+ `enum Color {..}` in header) | storage size |
|
|
51
|
+
| `bitfield[uint8_t, 3]` | `uint8_t x : 3` | shares a storage unit |
|
|
50
52
|
| another `@firstclass` class | `struct Name` | its size |
|
|
51
53
|
|
|
52
54
|
Values are checked: out of range ints, over-long strings and wrong lengths raise
|
|
53
55
|
`ValueError`; wrong Python types raise `TypeError`. Nothing is truncated or coerced silently.
|
|
54
56
|
|
|
57
|
+
**Enums.** Int-valued `Enum`/`IntEnum`/`IntFlag` classes. `enum_t[Color, uint8_t]` picks the width;
|
|
58
|
+
a bare `Color` annotation is 32-bit like a C `int` (unsigned unless a value is negative). Values
|
|
59
|
+
are stored, and written to JSON/XML, as ints; reading an unknown value raises `ValueError`.
|
|
60
|
+
`c_header()` emits `enum Color { Color_RED = 1, ... };`.
|
|
61
|
+
|
|
62
|
+
**Bitfields.** `bitfield[uint8_t, 3]` is 3 bits of a `uint8_t` unit, like `uint8_t x : 3`.
|
|
63
|
+
Neighbouring bitfields with the same storage type share a unit while they fit; otherwise a new
|
|
64
|
+
unit starts. Little-endian fills from the lowest bit, big-endian from the highest (what ctypes
|
|
65
|
+
does). Signed storage (`int8_t`) gives signed fields. Mixing storage types in one run is not
|
|
66
|
+
portable across C compilers; keep a run on one type. No arrays of bitfields.
|
|
67
|
+
|
|
55
68
|
Raw `ctypes` types work as annotations too (packed, same layout as a `_pack_ = 1` Structure):
|
|
56
69
|
`ctypes.c_int`, `c_uint64`, `c_size_t`, `c_bool`, `c_float`, `c_double`, `c_char`, `c_uint16 * 4`,
|
|
57
70
|
`c_char * 8`. Pointers (`c_void_p`, `POINTER(T)`, `c_char_p`) are stored as the address (an `int`,
|
|
@@ -64,6 +77,7 @@ address only means something inside the same process. Sizes follow the platform
|
|
|
64
77
|
| Method | |
|
|
65
78
|
|---|---|
|
|
66
79
|
| `obj.to_bytes()` / `Cls.from_bytes(data)` | binary; `from_bytes` needs exactly `sizeof()` bytes |
|
|
80
|
+
| `Cls.unpack_from(buffer, offset=0)` | like `struct.unpack_from`: read one object at `offset`, trailing bytes are fine; any buffer type |
|
|
67
81
|
| `Cls.sizeof()` | size in bytes |
|
|
68
82
|
| `obj.to_dict()` / `Cls.from_dict(d)` | JSON-compatible dict (bytes as hex) |
|
|
69
83
|
| `obj.to_json(**kw)` / `Cls.from_json(s)` | `kw` goes to `json.dumps` |
|
|
@@ -71,6 +85,33 @@ address only means something inside the same process. Sizes follow the platform
|
|
|
71
85
|
| `obj.to_ctypes()` / `Cls.from_ctypes(c)` / `Cls.ctypes_type()` | `ctypes.Structure` with matching layout |
|
|
72
86
|
| `Cls.c_header()` | C source for the struct(s), nested ones first |
|
|
73
87
|
|
|
88
|
+
## Import C/C++ headers
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from firstclass import from_header # also: firstclass.from_header
|
|
92
|
+
|
|
93
|
+
t = from_header("protocol.h") # or from_header(text="struct P { ... };")
|
|
94
|
+
pkt = t.Packet(1, True, ...) # structs -> @firstclass classes
|
|
95
|
+
t.Color.RED # enums / enum class -> IntEnum
|
|
96
|
+
t.constants["MAX_LEN"] # integer #defines, consts, enum members
|
|
97
|
+
t.skipped # {"name": "reason"} for anything not convertible
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Understood: `struct`/`class`/`typedef struct`, nested and anonymous-member structs, `enum` and
|
|
101
|
+
`enum class : type` (explicit values, implicit increments, constant expressions), bitfields, arrays
|
|
102
|
+
(multi-dimensional, sizes from `#define`/enum/`const`/`sizeof`), `char[N]` -> `char_array`,
|
|
103
|
+
`uint8_t[N]` -> `bytes_t`, pointers and function pointers (stored as addresses), `typedef`/`using`,
|
|
104
|
+
`#define`/`#if`/`#ifdef`/`#elif` (preset macros with `defines={...}`), `namespace`, `extern "C"`,
|
|
105
|
+
`#pragma pack(push/pop/N)` and `__attribute__((packed))`. Layout follows C: natural alignment
|
|
106
|
+
padding unless the struct is packed; force it with `align=True/False`. Options: `byteorder`,
|
|
107
|
+
`long_size` (size of C `long`; default is this machine's).
|
|
108
|
+
|
|
109
|
+
Skipped with a reason instead of failing: unions, flexible array members, unnamed padding
|
|
110
|
+
bitfields, inheritance, virtual members, unknown types (`#include`d headers are not read), `pack(2)`
|
|
111
|
+
/`pack(4)`, `aligned(N)`, `long double`, empty structs, anything depending on a skipped struct.
|
|
112
|
+
Plain C enums are 32-bit like C unless `: type` is given. C field names that are Python keywords
|
|
113
|
+
(`class`, `from`, ...) get a trailing underscore (`class_`).
|
|
114
|
+
|
|
74
115
|
## Layout options
|
|
75
116
|
|
|
76
117
|
```python
|
|
@@ -95,7 +136,8 @@ in a comment.
|
|
|
95
136
|
|
|
96
137
|
## Example
|
|
97
138
|
|
|
98
|
-
`python examples/example.py`
|
|
139
|
+
`python examples/example.py` defines a class in Python, then imports `examples/protocol.h` (enums, `enum class`,
|
|
140
|
+
bitfields, `#pragma pack` structs) and uses the generated classes.
|
|
99
141
|
|
|
100
142
|
## Develop
|
|
101
143
|
|
|
@@ -40,11 +40,24 @@ Packet.sizeof() # 36
|
|
|
40
40
|
| `char_array[N]` | `char x[N]` | N (UTF-8, NUL-padded) |
|
|
41
41
|
| `bytes_t[N]` | `uint8_t x[N]` | N |
|
|
42
42
|
| `array_t[T, N]` | `T x[N]` | N * size(T) |
|
|
43
|
+
| `enum_t[Color, uint8_t]` | `uint8_t x` (+ `enum Color {..}` in header) | storage size |
|
|
44
|
+
| `bitfield[uint8_t, 3]` | `uint8_t x : 3` | shares a storage unit |
|
|
43
45
|
| another `@firstclass` class | `struct Name` | its size |
|
|
44
46
|
|
|
45
47
|
Values are checked: out of range ints, over-long strings and wrong lengths raise
|
|
46
48
|
`ValueError`; wrong Python types raise `TypeError`. Nothing is truncated or coerced silently.
|
|
47
49
|
|
|
50
|
+
**Enums.** Int-valued `Enum`/`IntEnum`/`IntFlag` classes. `enum_t[Color, uint8_t]` picks the width;
|
|
51
|
+
a bare `Color` annotation is 32-bit like a C `int` (unsigned unless a value is negative). Values
|
|
52
|
+
are stored, and written to JSON/XML, as ints; reading an unknown value raises `ValueError`.
|
|
53
|
+
`c_header()` emits `enum Color { Color_RED = 1, ... };`.
|
|
54
|
+
|
|
55
|
+
**Bitfields.** `bitfield[uint8_t, 3]` is 3 bits of a `uint8_t` unit, like `uint8_t x : 3`.
|
|
56
|
+
Neighbouring bitfields with the same storage type share a unit while they fit; otherwise a new
|
|
57
|
+
unit starts. Little-endian fills from the lowest bit, big-endian from the highest (what ctypes
|
|
58
|
+
does). Signed storage (`int8_t`) gives signed fields. Mixing storage types in one run is not
|
|
59
|
+
portable across C compilers; keep a run on one type. No arrays of bitfields.
|
|
60
|
+
|
|
48
61
|
Raw `ctypes` types work as annotations too (packed, same layout as a `_pack_ = 1` Structure):
|
|
49
62
|
`ctypes.c_int`, `c_uint64`, `c_size_t`, `c_bool`, `c_float`, `c_double`, `c_char`, `c_uint16 * 4`,
|
|
50
63
|
`c_char * 8`. Pointers (`c_void_p`, `POINTER(T)`, `c_char_p`) are stored as the address (an `int`,
|
|
@@ -57,6 +70,7 @@ address only means something inside the same process. Sizes follow the platform
|
|
|
57
70
|
| Method | |
|
|
58
71
|
|---|---|
|
|
59
72
|
| `obj.to_bytes()` / `Cls.from_bytes(data)` | binary; `from_bytes` needs exactly `sizeof()` bytes |
|
|
73
|
+
| `Cls.unpack_from(buffer, offset=0)` | like `struct.unpack_from`: read one object at `offset`, trailing bytes are fine; any buffer type |
|
|
60
74
|
| `Cls.sizeof()` | size in bytes |
|
|
61
75
|
| `obj.to_dict()` / `Cls.from_dict(d)` | JSON-compatible dict (bytes as hex) |
|
|
62
76
|
| `obj.to_json(**kw)` / `Cls.from_json(s)` | `kw` goes to `json.dumps` |
|
|
@@ -64,6 +78,33 @@ address only means something inside the same process. Sizes follow the platform
|
|
|
64
78
|
| `obj.to_ctypes()` / `Cls.from_ctypes(c)` / `Cls.ctypes_type()` | `ctypes.Structure` with matching layout |
|
|
65
79
|
| `Cls.c_header()` | C source for the struct(s), nested ones first |
|
|
66
80
|
|
|
81
|
+
## Import C/C++ headers
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from firstclass import from_header # also: firstclass.from_header
|
|
85
|
+
|
|
86
|
+
t = from_header("protocol.h") # or from_header(text="struct P { ... };")
|
|
87
|
+
pkt = t.Packet(1, True, ...) # structs -> @firstclass classes
|
|
88
|
+
t.Color.RED # enums / enum class -> IntEnum
|
|
89
|
+
t.constants["MAX_LEN"] # integer #defines, consts, enum members
|
|
90
|
+
t.skipped # {"name": "reason"} for anything not convertible
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Understood: `struct`/`class`/`typedef struct`, nested and anonymous-member structs, `enum` and
|
|
94
|
+
`enum class : type` (explicit values, implicit increments, constant expressions), bitfields, arrays
|
|
95
|
+
(multi-dimensional, sizes from `#define`/enum/`const`/`sizeof`), `char[N]` -> `char_array`,
|
|
96
|
+
`uint8_t[N]` -> `bytes_t`, pointers and function pointers (stored as addresses), `typedef`/`using`,
|
|
97
|
+
`#define`/`#if`/`#ifdef`/`#elif` (preset macros with `defines={...}`), `namespace`, `extern "C"`,
|
|
98
|
+
`#pragma pack(push/pop/N)` and `__attribute__((packed))`. Layout follows C: natural alignment
|
|
99
|
+
padding unless the struct is packed; force it with `align=True/False`. Options: `byteorder`,
|
|
100
|
+
`long_size` (size of C `long`; default is this machine's).
|
|
101
|
+
|
|
102
|
+
Skipped with a reason instead of failing: unions, flexible array members, unnamed padding
|
|
103
|
+
bitfields, inheritance, virtual members, unknown types (`#include`d headers are not read), `pack(2)`
|
|
104
|
+
/`pack(4)`, `aligned(N)`, `long double`, empty structs, anything depending on a skipped struct.
|
|
105
|
+
Plain C enums are 32-bit like C unless `: type` is given. C field names that are Python keywords
|
|
106
|
+
(`class`, `from`, ...) get a trailing underscore (`class_`).
|
|
107
|
+
|
|
67
108
|
## Layout options
|
|
68
109
|
|
|
69
110
|
```python
|
|
@@ -88,7 +129,8 @@ in a comment.
|
|
|
88
129
|
|
|
89
130
|
## Example
|
|
90
131
|
|
|
91
|
-
`python examples/example.py`
|
|
132
|
+
`python examples/example.py` defines a class in Python, then imports `examples/protocol.h` (enums, `enum class`,
|
|
133
|
+
bitfields, `#pragma pack` structs) and uses the generated classes.
|
|
92
134
|
|
|
93
135
|
## Develop
|
|
94
136
|
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from ._core import firstclass
|
|
2
|
+
from ._header import from_header
|
|
2
3
|
from ._specs import (
|
|
3
4
|
array_t,
|
|
5
|
+
bitfield,
|
|
4
6
|
bool_t,
|
|
5
7
|
bytes_t,
|
|
6
8
|
char_array,
|
|
7
9
|
char_t,
|
|
10
|
+
enum_t,
|
|
8
11
|
float32_t,
|
|
9
12
|
float64_t,
|
|
10
13
|
int8_t,
|
|
@@ -18,9 +21,11 @@ from ._specs import (
|
|
|
18
21
|
)
|
|
19
22
|
|
|
20
23
|
__all__ = [
|
|
21
|
-
"firstclass",
|
|
24
|
+
"firstclass", "from_header",
|
|
22
25
|
"uint8_t", "uint16_t", "uint32_t", "uint64_t",
|
|
23
26
|
"int8_t", "int16_t", "int32_t", "int64_t",
|
|
24
27
|
"float32_t", "float64_t", "bool_t", "char_t",
|
|
25
|
-
"char_array", "bytes_t", "array_t",
|
|
28
|
+
"char_array", "bytes_t", "array_t", "bitfield", "enum_t",
|
|
26
29
|
]
|
|
30
|
+
|
|
31
|
+
firstclass.from_header = from_header # also reachable as the decorator's attribute
|
|
@@ -8,10 +8,10 @@ import sys
|
|
|
8
8
|
import typing
|
|
9
9
|
import xml.etree.ElementTree as ET
|
|
10
10
|
|
|
11
|
-
from ._specs import ArrayOf, Nested, Spec, resolve_spec
|
|
11
|
+
from ._specs import ArrayOf, BitField, EnumSpec, Nested, Spec, resolve_spec
|
|
12
12
|
|
|
13
13
|
_METHODS = (
|
|
14
|
-
"to_bytes", "from_bytes", "to_dict", "from_dict", "to_json", "from_json",
|
|
14
|
+
"to_bytes", "from_bytes", "unpack_from", "to_dict", "from_dict", "to_json", "from_json",
|
|
15
15
|
"to_xml", "from_xml", "to_ctypes", "from_ctypes", "ctypes_type", "c_header", "sizeof",
|
|
16
16
|
)
|
|
17
17
|
|
|
@@ -50,16 +50,35 @@ class Schema:
|
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
def layout(self):
|
|
53
|
+
"""-> (items, total_size, widest_alignment).
|
|
54
|
+
|
|
55
|
+
An item is ("field", index, offset) or ("bits", storage_spec, offset, [(index, shift, bits)]).
|
|
56
|
+
"""
|
|
53
57
|
if self._layout is None:
|
|
54
58
|
self.require_binary()
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
msb = self.bo == ">"
|
|
60
|
+
off, widest, items, group = 0, 1, [], None # group = [storage, used_bits, members]
|
|
61
|
+
for i, (_, spec) in enumerate(self.fields):
|
|
62
|
+
if isinstance(spec, BitField):
|
|
63
|
+
st, unit = spec.storage, spec.storage.size() * 8
|
|
64
|
+
if group is None or group[0].name != st.name or group[1] + spec.bits > unit:
|
|
65
|
+
a = st.align() if self.aligned else 1
|
|
66
|
+
off = _round_up(off, a)
|
|
67
|
+
widest = max(widest, a)
|
|
68
|
+
group = [st, 0, []]
|
|
69
|
+
items.append(("bits", st, off, group[2]))
|
|
70
|
+
off += st.size()
|
|
71
|
+
shift = unit - group[1] - spec.bits if msb else group[1]
|
|
72
|
+
group[2].append((i, shift, spec.bits))
|
|
73
|
+
group[1] += spec.bits
|
|
74
|
+
continue
|
|
75
|
+
group = None
|
|
57
76
|
a = spec.align() if self.aligned else 1
|
|
58
77
|
off = _round_up(off, a)
|
|
59
|
-
|
|
78
|
+
items.append(("field", i, off))
|
|
60
79
|
off += spec.size()
|
|
61
80
|
widest = max(widest, a)
|
|
62
|
-
self._layout = (
|
|
81
|
+
self._layout = (items, _round_up(off, widest), widest)
|
|
63
82
|
return self._layout
|
|
64
83
|
|
|
65
84
|
def size(self) -> int:
|
|
@@ -81,35 +100,71 @@ class Schema:
|
|
|
81
100
|
return {f.name: f for f, _ in self.fields} # small; fields are cached
|
|
82
101
|
|
|
83
102
|
def _ctx(self, f, exc):
|
|
84
|
-
|
|
103
|
+
kind = TypeError if isinstance(exc, TypeError) else ValueError # e.g. UnicodeDecodeError -> ValueError
|
|
104
|
+
return kind(f"{self.cls.__name__}.{f.name}: {exc}")
|
|
85
105
|
|
|
86
106
|
# -- bytes ---------------------------------------------------------------
|
|
87
107
|
|
|
88
108
|
def pack(self, obj) -> bytes:
|
|
89
|
-
|
|
109
|
+
items, total, _ = self.layout()
|
|
90
110
|
buf = bytearray(total)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
order = "little" if self.bo == "<" else "big"
|
|
112
|
+
for item in items:
|
|
113
|
+
if item[0] == "field":
|
|
114
|
+
f, spec = self.fields[item[1]]
|
|
115
|
+
try:
|
|
116
|
+
raw = spec.pack(getattr(obj, f.name), self.bo)
|
|
117
|
+
except (TypeError, ValueError) as e:
|
|
118
|
+
raise self._ctx(f, e) from e
|
|
119
|
+
buf[item[2]:item[2] + len(raw)] = raw
|
|
120
|
+
continue
|
|
121
|
+
_, st, off, members = item
|
|
122
|
+
unit = 0
|
|
123
|
+
for i, shift, bits in members:
|
|
124
|
+
f, spec = self.fields[i]
|
|
125
|
+
try:
|
|
126
|
+
unit |= (spec.check(getattr(obj, f.name)) & ((1 << bits) - 1)) << shift
|
|
127
|
+
except (TypeError, ValueError) as e:
|
|
128
|
+
raise self._ctx(f, e) from e
|
|
129
|
+
buf[off:off + st.size()] = unit.to_bytes(st.size(), order)
|
|
97
130
|
return bytes(buf)
|
|
98
131
|
|
|
99
132
|
def unpack(self, buf, off: int = 0):
|
|
100
|
-
|
|
133
|
+
items, total, _ = self.layout()
|
|
101
134
|
if len(buf) - off < total:
|
|
102
135
|
raise ValueError(f"{self.cls.__name__} needs {total} bytes, got {len(buf) - off}")
|
|
136
|
+
order = "little" if self.bo == "<" else "big"
|
|
103
137
|
kw = {}
|
|
104
|
-
for
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
138
|
+
for item in items:
|
|
139
|
+
if item[0] == "field":
|
|
140
|
+
f, spec = self.fields[item[1]]
|
|
141
|
+
try:
|
|
142
|
+
kw[f.name] = spec.unpack(buf, off + item[2], self.bo)
|
|
143
|
+
except (TypeError, ValueError) as e:
|
|
144
|
+
raise self._ctx(f, e) from e
|
|
145
|
+
continue
|
|
146
|
+
_, st, o, members = item
|
|
147
|
+
unit = int.from_bytes(bytes(buf[off + o:off + o + st.size()]), order)
|
|
148
|
+
for i, shift, bits in members:
|
|
149
|
+
f, spec = self.fields[i]
|
|
150
|
+
v = (unit >> shift) & ((1 << bits) - 1)
|
|
151
|
+
if spec.signed and v >> (bits - 1):
|
|
152
|
+
v -= 1 << bits
|
|
153
|
+
kw[f.name] = v
|
|
109
154
|
return self._build(kw)
|
|
110
155
|
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _as_bytes_like(data):
|
|
158
|
+
return data if isinstance(data, (bytes, bytearray)) else memoryview(data).cast("B")
|
|
159
|
+
|
|
160
|
+
def unpack_from(self, buffer, offset: int = 0):
|
|
161
|
+
"""Like struct.unpack_from: read one object at `offset`; extra bytes after it are fine."""
|
|
162
|
+
if isinstance(offset, bool) or not isinstance(offset, int) or offset < 0:
|
|
163
|
+
raise ValueError(f"offset must be a non-negative int, got {offset!r}")
|
|
164
|
+
return self.unpack(self._as_bytes_like(buffer), offset)
|
|
165
|
+
|
|
111
166
|
def from_bytes(self, data):
|
|
112
|
-
data =
|
|
167
|
+
data = self._as_bytes_like(data)
|
|
113
168
|
if len(data) != self.size():
|
|
114
169
|
raise ValueError(f"{self.cls.__name__} needs exactly {self.size()} bytes, got {len(data)}")
|
|
115
170
|
return self.unpack(data, 0)
|
|
@@ -160,11 +215,15 @@ class Schema:
|
|
|
160
215
|
if self._ctype is None:
|
|
161
216
|
self.require_binary()
|
|
162
217
|
base = ctypes.LittleEndianStructure if self.bo == "<" else ctypes.BigEndianStructure
|
|
163
|
-
ns = {"_fields_": [
|
|
218
|
+
ns = {"_fields_": [
|
|
219
|
+
(f.name, spec.ctype(self.bo), spec.bits) if isinstance(spec, BitField)
|
|
220
|
+
else (f.name, spec.ctype(self.bo))
|
|
221
|
+
for f, spec in self.fields
|
|
222
|
+
]}
|
|
164
223
|
if not self.aligned:
|
|
165
224
|
ns["_pack_"] = 1
|
|
166
|
-
|
|
167
|
-
|
|
225
|
+
if sys.version_info >= (3, 14):
|
|
226
|
+
ns["_layout_"] = "ms" # our bitfield rules; also silences the _pack_ warning
|
|
168
227
|
self._ctype = type(self.cls.__name__ + "_c", (base,), ns)
|
|
169
228
|
return self._ctype
|
|
170
229
|
|
|
@@ -197,6 +256,12 @@ class Schema:
|
|
|
197
256
|
seen.add(schema.cls)
|
|
198
257
|
for dep in schema._nested_schemas():
|
|
199
258
|
emit(dep)
|
|
259
|
+
for _, spec in schema.fields:
|
|
260
|
+
while isinstance(spec, ArrayOf):
|
|
261
|
+
spec = spec.elem
|
|
262
|
+
if isinstance(spec, EnumSpec) and spec.cls not in seen:
|
|
263
|
+
seen.add(spec.cls)
|
|
264
|
+
out.append(spec.c_definition())
|
|
200
265
|
lines = [f"/* byteorder: {schema.byteorder} */"]
|
|
201
266
|
if not schema.aligned:
|
|
202
267
|
lines.append("#pragma pack(push, 1)")
|
|
@@ -219,6 +284,8 @@ def firstclass(cls=None, /, *, byteorder: str = "little", align: bool = False, *
|
|
|
219
284
|
"""
|
|
220
285
|
|
|
221
286
|
def wrap(cls):
|
|
287
|
+
if not isinstance(cls, type):
|
|
288
|
+
raise TypeError(f"@firstclass needs a class, got {type(cls).__name__}")
|
|
222
289
|
dc = dataclasses.dataclass(cls, **dataclass_kwargs)
|
|
223
290
|
clash = [f.name for f in dataclasses.fields(dc) if f.name in _METHODS or f.name == "__firstclass__"]
|
|
224
291
|
if clash:
|
|
@@ -227,12 +294,13 @@ def firstclass(cls=None, /, *, byteorder: str = "little", align: bool = False, *
|
|
|
227
294
|
dc.__firstclass__ = s
|
|
228
295
|
dc.to_bytes = lambda self: s.pack(self)
|
|
229
296
|
dc.from_bytes = staticmethod(s.from_bytes)
|
|
297
|
+
dc.unpack_from = staticmethod(s.unpack_from)
|
|
230
298
|
dc.to_dict = lambda self: s.to_dict(self)
|
|
231
299
|
dc.from_dict = staticmethod(s.from_dict)
|
|
232
300
|
dc.to_json = lambda self, **kw: json.dumps(s.to_dict(self), **kw)
|
|
233
301
|
dc.from_json = staticmethod(lambda text: s.from_dict(json.loads(text)))
|
|
234
302
|
dc.to_xml = lambda self, indent=None: _to_xml(s, self, indent)
|
|
235
|
-
dc.from_xml = staticmethod(lambda text: s.read_xml(
|
|
303
|
+
dc.from_xml = staticmethod(lambda text: s.read_xml(_parse_xml(text)))
|
|
236
304
|
dc.to_ctypes = lambda self: s.to_ctypes(self)
|
|
237
305
|
dc.from_ctypes = staticmethod(s.from_ctypes)
|
|
238
306
|
dc.ctypes_type = staticmethod(s.ctypes_type)
|
|
@@ -243,6 +311,13 @@ def firstclass(cls=None, /, *, byteorder: str = "little", align: bool = False, *
|
|
|
243
311
|
return wrap if cls is None else wrap(cls)
|
|
244
312
|
|
|
245
313
|
|
|
314
|
+
def _parse_xml(text):
|
|
315
|
+
try:
|
|
316
|
+
return ET.fromstring(text)
|
|
317
|
+
except ET.ParseError as e:
|
|
318
|
+
raise ValueError(f"malformed XML: {e}") from e
|
|
319
|
+
|
|
320
|
+
|
|
246
321
|
def _to_xml(schema: Schema, obj, indent) -> str:
|
|
247
322
|
root = ET.Element(schema.cls.__name__)
|
|
248
323
|
schema.fill_xml(obj, root)
|