sltcodec 1.1.0__tar.gz → 1.2.1__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.
- {sltcodec-1.1.0 → sltcodec-1.2.1}/PKG-INFO +46 -4
- {sltcodec-1.1.0 → sltcodec-1.2.1}/README.md +45 -3
- {sltcodec-1.1.0 → sltcodec-1.2.1}/pyproject.toml +1 -1
- {sltcodec-1.1.0 → sltcodec-1.2.1}/src/sltcodec/codec.py +1 -1
- {sltcodec-1.1.0 → sltcodec-1.2.1}/src/sltcodec/types.py +19 -37
- {sltcodec-1.1.0 → sltcodec-1.2.1}/tests/test_codec.py +14 -16
- {sltcodec-1.1.0 → sltcodec-1.2.1}/uv.lock +1 -1
- {sltcodec-1.1.0 → sltcodec-1.2.1}/.github/workflows/publish_to_pypi.yml +0 -0
- {sltcodec-1.1.0 → sltcodec-1.2.1}/.github/workflows/publish_to_testpypi.yml +0 -0
- {sltcodec-1.1.0 → sltcodec-1.2.1}/.gitignore +0 -0
- {sltcodec-1.1.0 → sltcodec-1.2.1}/.python-version +0 -0
- {sltcodec-1.1.0 → sltcodec-1.2.1}/.vscode/launch.json +0 -0
- {sltcodec-1.1.0 → sltcodec-1.2.1}/.vscode/settings.json +0 -0
- {sltcodec-1.1.0 → sltcodec-1.2.1}/LICENSE +0 -0
- {sltcodec-1.1.0 → sltcodec-1.2.1}/bump_major.ps1 +0 -0
- {sltcodec-1.1.0 → sltcodec-1.2.1}/bump_minor.ps1 +0 -0
- {sltcodec-1.1.0 → sltcodec-1.2.1}/bump_patch.ps1 +0 -0
- {sltcodec-1.1.0 → sltcodec-1.2.1}/src/sltcodec/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sltcodec
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Decode and encode bytearrays according to struct layout definitions using sltcore.
|
|
5
5
|
Project-URL: Homepage, https://github.com/fangface-hub/StructLayoutToolkitCodec
|
|
6
6
|
Project-URL: Documentation, https://readthedocs.org
|
|
@@ -145,7 +145,7 @@ Output:
|
|
|
145
145
|
```python
|
|
146
146
|
bytearray(b"\x03\x04")
|
|
147
147
|
StructInstance(struct_def=StructDef(...), field_instances=[...])
|
|
148
|
-
[FieldInstance(field_def=FieldDef(name='pair', offset=InfoSize(byte=0, bit=0), size=InfoSize(byte=2, bit=0), type=
|
|
148
|
+
[FieldInstance(field_def=FieldDef(name='pair', offset=InfoSize(byte=0, bit=0), size=InfoSize(byte=2, bit=0), type=StructDef(...), scale=1.0, repeat=None),
|
|
149
149
|
value=StructInstance(struct_def=StructDef(...), field_instances=[...]))]
|
|
150
150
|
```
|
|
151
151
|
|
|
@@ -204,6 +204,10 @@ print(encoded_float, decoded_float)
|
|
|
204
204
|
You can persist reusable structure definitions by name with
|
|
205
205
|
`save_struct_def_dict` / `load_struct_def_dict`.
|
|
206
206
|
|
|
207
|
+
`InfoSize` values in `offset` and `size` are saved as typed dictionaries in
|
|
208
|
+
JSON. Expression-based offsets and sizes remain strings and are resolved when
|
|
209
|
+
the definition is used.
|
|
210
|
+
|
|
207
211
|
```python
|
|
208
212
|
from pathlib import Path
|
|
209
213
|
|
|
@@ -232,10 +236,48 @@ print(loaded["Header"].description)
|
|
|
232
236
|
Example output:
|
|
233
237
|
|
|
234
238
|
```python
|
|
235
|
-
|
|
236
|
-
|
|
239
|
+
Header
|
|
240
|
+
Simple header
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Enum Definitions
|
|
244
|
+
|
|
245
|
+
`FieldDef.enum_def_name` holds only the name of an `EnumDef`, not the definition
|
|
246
|
+
itself. Pass the actual definitions via an `enum_def_dict` (mapping name to
|
|
247
|
+
`EnumDef`) to `decode` / `encode` so the matching `EnumDef` can be resolved and
|
|
248
|
+
attached as `FieldInstance.enum_item`.
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
from sltcore import InfoSize
|
|
252
|
+
from sltcodec import EnumDef, FieldDef, decode
|
|
253
|
+
|
|
254
|
+
status_enum = EnumDef(name="Status", values={"OK": 0, "NG": 1})
|
|
255
|
+
|
|
256
|
+
struct_def = [
|
|
257
|
+
FieldDef(name="status",
|
|
258
|
+
offset=InfoSize(0, 0),
|
|
259
|
+
size=InfoSize(1, 0),
|
|
260
|
+
type="unsigned int",
|
|
261
|
+
enum_def_name="Status"),
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
decoded = decode(struct_def, bytearray(b"\x01"),
|
|
265
|
+
enum_def_dict={"Status": status_enum})
|
|
266
|
+
|
|
267
|
+
print(decoded.field_instances[0].value)
|
|
268
|
+
print(decoded.field_instances[0].enum_item)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Output:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
1
|
|
275
|
+
('NG', 1)
|
|
237
276
|
```
|
|
238
277
|
|
|
278
|
+
You can also persist enum definitions by name with
|
|
279
|
+
`save_enum_def_dict` / `load_enum_def_dict`, mirroring `StructDef` dictionaries.
|
|
280
|
+
|
|
239
281
|
## Development
|
|
240
282
|
|
|
241
283
|
```bash
|
|
@@ -128,7 +128,7 @@ Output:
|
|
|
128
128
|
```python
|
|
129
129
|
bytearray(b"\x03\x04")
|
|
130
130
|
StructInstance(struct_def=StructDef(...), field_instances=[...])
|
|
131
|
-
[FieldInstance(field_def=FieldDef(name='pair', offset=InfoSize(byte=0, bit=0), size=InfoSize(byte=2, bit=0), type=
|
|
131
|
+
[FieldInstance(field_def=FieldDef(name='pair', offset=InfoSize(byte=0, bit=0), size=InfoSize(byte=2, bit=0), type=StructDef(...), scale=1.0, repeat=None),
|
|
132
132
|
value=StructInstance(struct_def=StructDef(...), field_instances=[...]))]
|
|
133
133
|
```
|
|
134
134
|
|
|
@@ -187,6 +187,10 @@ print(encoded_float, decoded_float)
|
|
|
187
187
|
You can persist reusable structure definitions by name with
|
|
188
188
|
`save_struct_def_dict` / `load_struct_def_dict`.
|
|
189
189
|
|
|
190
|
+
`InfoSize` values in `offset` and `size` are saved as typed dictionaries in
|
|
191
|
+
JSON. Expression-based offsets and sizes remain strings and are resolved when
|
|
192
|
+
the definition is used.
|
|
193
|
+
|
|
190
194
|
```python
|
|
191
195
|
from pathlib import Path
|
|
192
196
|
|
|
@@ -215,10 +219,48 @@ print(loaded["Header"].description)
|
|
|
215
219
|
Example output:
|
|
216
220
|
|
|
217
221
|
```python
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
Header
|
|
223
|
+
Simple header
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Enum Definitions
|
|
227
|
+
|
|
228
|
+
`FieldDef.enum_def_name` holds only the name of an `EnumDef`, not the definition
|
|
229
|
+
itself. Pass the actual definitions via an `enum_def_dict` (mapping name to
|
|
230
|
+
`EnumDef`) to `decode` / `encode` so the matching `EnumDef` can be resolved and
|
|
231
|
+
attached as `FieldInstance.enum_item`.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from sltcore import InfoSize
|
|
235
|
+
from sltcodec import EnumDef, FieldDef, decode
|
|
236
|
+
|
|
237
|
+
status_enum = EnumDef(name="Status", values={"OK": 0, "NG": 1})
|
|
238
|
+
|
|
239
|
+
struct_def = [
|
|
240
|
+
FieldDef(name="status",
|
|
241
|
+
offset=InfoSize(0, 0),
|
|
242
|
+
size=InfoSize(1, 0),
|
|
243
|
+
type="unsigned int",
|
|
244
|
+
enum_def_name="Status"),
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
decoded = decode(struct_def, bytearray(b"\x01"),
|
|
248
|
+
enum_def_dict={"Status": status_enum})
|
|
249
|
+
|
|
250
|
+
print(decoded.field_instances[0].value)
|
|
251
|
+
print(decoded.field_instances[0].enum_item)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Output:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
1
|
|
258
|
+
('NG', 1)
|
|
220
259
|
```
|
|
221
260
|
|
|
261
|
+
You can also persist enum definitions by name with
|
|
262
|
+
`save_enum_def_dict` / `load_enum_def_dict`, mirroring `StructDef` dictionaries.
|
|
263
|
+
|
|
222
264
|
## Development
|
|
223
265
|
|
|
224
266
|
```bash
|
|
@@ -368,7 +368,7 @@ def decode_field(
|
|
|
368
368
|
repeat=field_def.repeat,
|
|
369
369
|
description=field_def.description,
|
|
370
370
|
range_expression=field_def.range_expression,
|
|
371
|
-
|
|
371
|
+
enum_def_name=field_def.enum_def_name,
|
|
372
372
|
byte_swap=byte_swap,
|
|
373
373
|
)
|
|
374
374
|
if isinstance(resolved_type, StructDef):
|
|
@@ -104,8 +104,9 @@ class FieldDef:
|
|
|
104
104
|
default=None, metadata={"desc": "The description of the field"})
|
|
105
105
|
range_expression: str | None = field(
|
|
106
106
|
default=None, metadata={"desc": "The value range expression"})
|
|
107
|
-
|
|
108
|
-
default=None,
|
|
107
|
+
enum_def_name: str | None = field(
|
|
108
|
+
default=None,
|
|
109
|
+
metadata={"desc": "The name of the enum definition for the field"})
|
|
109
110
|
byte_swap: bool | str = field(
|
|
110
111
|
default=False,
|
|
111
112
|
metadata={"desc": "Whether to reverse the field byte order"})
|
|
@@ -137,7 +138,7 @@ class FieldDef:
|
|
|
137
138
|
repeat=None,
|
|
138
139
|
description=self.description,
|
|
139
140
|
range_expression=split_range_expression,
|
|
140
|
-
|
|
141
|
+
enum_def_name=self.enum_def_name,
|
|
141
142
|
byte_swap=split_byte_swap)
|
|
142
143
|
|
|
143
144
|
@staticmethod
|
|
@@ -158,17 +159,19 @@ class FieldDef:
|
|
|
158
159
|
|
|
159
160
|
def to_dict(self) -> dict[str, Any]:
|
|
160
161
|
"""Convert this field definition to a JSON-serializable dictionary."""
|
|
162
|
+
|
|
163
|
+
def _serialize_info_like(value: Any) -> Any:
|
|
164
|
+
if isinstance(value, (Info, InfoSize)):
|
|
165
|
+
return json.loads(value.to_json())
|
|
166
|
+
return value
|
|
167
|
+
|
|
161
168
|
return {
|
|
162
169
|
"name":
|
|
163
170
|
self.name,
|
|
164
171
|
"offset":
|
|
165
|
-
self.offset
|
|
166
|
-
(Info,
|
|
167
|
-
InfoSize)) else self.offset,
|
|
172
|
+
_serialize_info_like(self.offset),
|
|
168
173
|
"size":
|
|
169
|
-
self.size
|
|
170
|
-
(Info,
|
|
171
|
-
InfoSize)) else self.size,
|
|
174
|
+
_serialize_info_like(self.size),
|
|
172
175
|
"type": {
|
|
173
176
|
"__type__": "StructDef",
|
|
174
177
|
"fields": self.type.to_dict(),
|
|
@@ -184,8 +187,8 @@ class FieldDef:
|
|
|
184
187
|
self.description,
|
|
185
188
|
"range_expression":
|
|
186
189
|
self.range_expression,
|
|
187
|
-
"
|
|
188
|
-
|
|
190
|
+
"enum_def_name":
|
|
191
|
+
self.enum_def_name,
|
|
189
192
|
"byte_swap":
|
|
190
193
|
self.byte_swap,
|
|
191
194
|
}
|
|
@@ -205,7 +208,7 @@ class FieldDef:
|
|
|
205
208
|
repeat = data.get("repeat")
|
|
206
209
|
description = data.get("description")
|
|
207
210
|
range_expression = data.get("range_expression")
|
|
208
|
-
|
|
211
|
+
enum_def_name = data.get("enum_def_name")
|
|
209
212
|
byte_swap = data.get("byte_swap", False)
|
|
210
213
|
|
|
211
214
|
def _deserialize_info_like(value: Any) -> Any:
|
|
@@ -242,14 +245,6 @@ class FieldDef:
|
|
|
242
245
|
else:
|
|
243
246
|
type_value = type_data
|
|
244
247
|
|
|
245
|
-
if enum_def_data is None:
|
|
246
|
-
enum_def = None
|
|
247
|
-
elif (isinstance(enum_def_data, dict)
|
|
248
|
-
and enum_def_data.get("__type__") == "EnumDef"):
|
|
249
|
-
enum_def = EnumDef.deserialize(enum_def_data)
|
|
250
|
-
else:
|
|
251
|
-
enum_def = None
|
|
252
|
-
|
|
253
248
|
return cls(
|
|
254
249
|
name=name,
|
|
255
250
|
offset=offset,
|
|
@@ -259,7 +254,7 @@ class FieldDef:
|
|
|
259
254
|
repeat=repeat,
|
|
260
255
|
description=description,
|
|
261
256
|
range_expression=range_expression,
|
|
262
|
-
|
|
257
|
+
enum_def_name=enum_def_name,
|
|
263
258
|
byte_swap=byte_swap,
|
|
264
259
|
)
|
|
265
260
|
|
|
@@ -279,7 +274,7 @@ class FieldDef:
|
|
|
279
274
|
-1 if self.repeat is None else self.repeat,
|
|
280
275
|
"" if self.description is None else self.description,
|
|
281
276
|
"" if self.range_expression is None else self.range_expression,
|
|
282
|
-
self.
|
|
277
|
+
"" if self.enum_def_name is None else self.enum_def_name,
|
|
283
278
|
self._sortable_byte_swap(self.byte_swap),
|
|
284
279
|
)
|
|
285
280
|
|
|
@@ -297,19 +292,6 @@ class FieldDef:
|
|
|
297
292
|
return (0, json.dumps(value.to_dict(), sort_keys=True))
|
|
298
293
|
return (1, value)
|
|
299
294
|
|
|
300
|
-
@staticmethod
|
|
301
|
-
def _sortable_enum_type(value: EnumDef | None) -> str:
|
|
302
|
-
"""Build a comparable key for enum definition metadata."""
|
|
303
|
-
if value is None:
|
|
304
|
-
return ""
|
|
305
|
-
return json.dumps(
|
|
306
|
-
{
|
|
307
|
-
"name": value.name,
|
|
308
|
-
"description": value.description,
|
|
309
|
-
"values": value.values,
|
|
310
|
-
},
|
|
311
|
-
sort_keys=True)
|
|
312
|
-
|
|
313
295
|
@staticmethod
|
|
314
296
|
def _sortable_byte_swap(value: bool | str) -> tuple[int, str]:
|
|
315
297
|
"""Build a comparable key for a byte-swap flag or expression."""
|
|
@@ -369,10 +351,10 @@ class FieldInstance:
|
|
|
369
351
|
field_def: FieldDef,
|
|
370
352
|
enum_def_dict: dict[str, EnumDef] | None) -> EnumDef | None:
|
|
371
353
|
"""Resolve enum definition from field metadata or lookup dictionary."""
|
|
372
|
-
if field_def.enum_def is not None:
|
|
373
|
-
return field_def.enum_def
|
|
374
354
|
if not enum_def_dict:
|
|
375
355
|
return None
|
|
356
|
+
if field_def.enum_def_name is not None:
|
|
357
|
+
return enum_def_dict.get(field_def.enum_def_name)
|
|
376
358
|
if isinstance(field_def.type, str) and field_def.type in enum_def_dict:
|
|
377
359
|
return enum_def_dict[field_def.type]
|
|
378
360
|
return enum_def_dict.get(field_def.name)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Tests for the sltcodec module."""
|
|
2
|
+
import json
|
|
2
3
|
from enum import Enum
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
@@ -179,7 +180,7 @@ def test_repeated_field_preserves_range_and_enum_metadata():
|
|
|
179
180
|
type="unsigned int",
|
|
180
181
|
repeat=2,
|
|
181
182
|
range_expression="0 <= value <= 255",
|
|
182
|
-
|
|
183
|
+
enum_def_name=enum_def.name)
|
|
183
184
|
|
|
184
185
|
encoded = encode(
|
|
185
186
|
StructInstance(struct_def=StructDef(fields=[field_def]),
|
|
@@ -192,8 +193,8 @@ def test_repeated_field_preserves_range_and_enum_metadata():
|
|
|
192
193
|
"0 <= value[0] <= 255")
|
|
193
194
|
assert decoded.field_instances[1].field_def.range_expression == (
|
|
194
195
|
"0 <= value[1] <= 255")
|
|
195
|
-
assert decoded.field_instances[0].field_def.
|
|
196
|
-
assert decoded.field_instances[1].field_def.
|
|
196
|
+
assert decoded.field_instances[0].field_def.enum_def_name == enum_def.name
|
|
197
|
+
assert decoded.field_instances[1].field_def.enum_def_name == enum_def.name
|
|
197
198
|
|
|
198
199
|
|
|
199
200
|
def test_split_repeat_replaces_name_in_range_expression():
|
|
@@ -242,23 +243,15 @@ def test_field_def_to_json_from_json_round_trip():
|
|
|
242
243
|
repeat=3,
|
|
243
244
|
description="A repeated value",
|
|
244
245
|
range_expression="0 <= value <= 255",
|
|
245
|
-
|
|
246
|
+
enum_def_name=enum_def.name)
|
|
246
247
|
|
|
247
248
|
payload = field_def.to_json()
|
|
248
249
|
restored = FieldDef.from_json(payload)
|
|
249
250
|
|
|
250
251
|
assert restored == field_def
|
|
251
252
|
assert restored.range_expression == "0 <= value <= 255"
|
|
252
|
-
assert restored.
|
|
253
|
-
assert restored.to_dict()["
|
|
254
|
-
"__type__": "EnumDef",
|
|
255
|
-
"name": "ValueKind",
|
|
256
|
-
"description": None,
|
|
257
|
-
"values": {
|
|
258
|
-
member.name: member.value
|
|
259
|
-
for member in ValueKind
|
|
260
|
-
},
|
|
261
|
-
}
|
|
253
|
+
assert restored.enum_def_name == enum_def.name
|
|
254
|
+
assert restored.to_dict()["enum_def_name"] == "ValueKind"
|
|
262
255
|
|
|
263
256
|
|
|
264
257
|
def test_field_def_byte_swap_json_round_trip():
|
|
@@ -425,9 +418,10 @@ def test_field_instance_from_value_sets_enum_item_from_field_enum_def():
|
|
|
425
418
|
offset=InfoSize(0, 0),
|
|
426
419
|
size=InfoSize(1, 0),
|
|
427
420
|
type="unsigned int",
|
|
428
|
-
|
|
421
|
+
enum_def_name=enum_def.name)
|
|
429
422
|
|
|
430
|
-
field_instance = FieldInstance.from_value(
|
|
423
|
+
field_instance = FieldInstance.from_value(
|
|
424
|
+
field_def, 1, enum_def_dict={enum_def.name: enum_def})
|
|
431
425
|
|
|
432
426
|
assert field_instance.enum_item == ("MANUAL", 1)
|
|
433
427
|
|
|
@@ -480,6 +474,10 @@ def test_struct_def_dict_load_and_save(tmp_path: Path):
|
|
|
480
474
|
path = tmp_path / "field_defs.json"
|
|
481
475
|
|
|
482
476
|
save_struct_def_dict(path, {"value": struct_def})
|
|
477
|
+
saved_field = json.loads(path.read_text())["value"]["fields"][0]
|
|
478
|
+
|
|
479
|
+
assert isinstance(saved_field["offset"], dict)
|
|
480
|
+
assert isinstance(saved_field["size"], dict)
|
|
483
481
|
loaded = load_struct_def_dict(path)
|
|
484
482
|
|
|
485
483
|
assert loaded["value"] == struct_def
|
|
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
|