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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firstclass
3
- Version: 0.0.2
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` shows bytes, JSON, XML, ctypes and the C header from one class.
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` shows bytes, JSON, XML, ctypes and the C header from one class.
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
- off, widest, offsets = 0, 1, []
56
- for _, spec in self.fields:
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
- offsets.append(off)
78
+ items.append(("field", i, off))
60
79
  off += spec.size()
61
80
  widest = max(widest, a)
62
- self._layout = (offsets, _round_up(off, widest), widest)
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
- return type(exc)(f"{self.cls.__name__}.{f.name}: {exc}")
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
- offsets, total, _ = self.layout()
109
+ items, total, _ = self.layout()
90
110
  buf = bytearray(total)
91
- for (f, spec), off in zip(self.fields, offsets):
92
- try:
93
- raw = spec.pack(getattr(obj, f.name), self.bo)
94
- except (TypeError, ValueError) as e:
95
- raise self._ctx(f, e) from e
96
- buf[off:off + len(raw)] = raw
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
- offsets, total, _ = self.layout()
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 (f, spec), o in zip(self.fields, offsets):
105
- try:
106
- kw[f.name] = spec.unpack(buf, off + o, self.bo)
107
- except (TypeError, ValueError) as e:
108
- raise self._ctx(f, e) from e
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 = memoryview(data).cast("B") if not isinstance(data, (bytes, bytearray)) else 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_": [(f.name, spec.ctype(self.bo)) for f, spec in self.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
- if sys.version_info >= (3, 14):
167
- ns["_layout_"] = "ms"
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(ET.fromstring(text)))
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)