sltcodec 1.0.0__tar.gz → 1.2.0__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.5
2
2
  Name: sltcodec
3
- Version: 1.0.0
3
+ Version: 1.2.0
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
@@ -236,6 +236,44 @@ bytearray(b"\x01\x07") StructInstance(struct_def=StructDef(...), field_instances
236
236
  bytearray(b"\x02?\xc0\x00\x00") StructInstance(struct_def=StructDef(...), field_instances=[...])
237
237
  ```
238
238
 
239
+ ## Enum Definitions
240
+
241
+ `FieldDef.enum_def_name` holds only the name of an `EnumDef`, not the definition
242
+ itself. Pass the actual definitions via an `enum_def_dict` (mapping name to
243
+ `EnumDef`) to `decode` / `encode` so the matching `EnumDef` can be resolved and
244
+ attached as `FieldInstance.enum_item`.
245
+
246
+ ```python
247
+ from sltcore import InfoSize
248
+ from sltcodec import EnumDef, FieldDef, decode
249
+
250
+ status_enum = EnumDef(name="Status", values={"OK": 0, "NG": 1})
251
+
252
+ struct_def = [
253
+ FieldDef(name="status",
254
+ offset=InfoSize(0, 0),
255
+ size=InfoSize(1, 0),
256
+ type="unsigned int",
257
+ enum_def_name="Status"),
258
+ ]
259
+
260
+ decoded = decode(struct_def, bytearray(b"\x01"),
261
+ enum_def_dict={"Status": status_enum})
262
+
263
+ print(decoded.field_instances[0].value)
264
+ print(decoded.field_instances[0].enum_item)
265
+ ```
266
+
267
+ Output:
268
+
269
+ ```python
270
+ 1
271
+ ('NG', 1)
272
+ ```
273
+
274
+ You can also persist enum definitions by name with
275
+ `save_enum_def_dict` / `load_enum_def_dict`, mirroring `StructDef` dictionaries.
276
+
239
277
  ## Development
240
278
 
241
279
  ```bash
@@ -219,6 +219,44 @@ bytearray(b"\x01\x07") StructInstance(struct_def=StructDef(...), field_instances
219
219
  bytearray(b"\x02?\xc0\x00\x00") StructInstance(struct_def=StructDef(...), field_instances=[...])
220
220
  ```
221
221
 
222
+ ## Enum Definitions
223
+
224
+ `FieldDef.enum_def_name` holds only the name of an `EnumDef`, not the definition
225
+ itself. Pass the actual definitions via an `enum_def_dict` (mapping name to
226
+ `EnumDef`) to `decode` / `encode` so the matching `EnumDef` can be resolved and
227
+ attached as `FieldInstance.enum_item`.
228
+
229
+ ```python
230
+ from sltcore import InfoSize
231
+ from sltcodec import EnumDef, FieldDef, decode
232
+
233
+ status_enum = EnumDef(name="Status", values={"OK": 0, "NG": 1})
234
+
235
+ struct_def = [
236
+ FieldDef(name="status",
237
+ offset=InfoSize(0, 0),
238
+ size=InfoSize(1, 0),
239
+ type="unsigned int",
240
+ enum_def_name="Status"),
241
+ ]
242
+
243
+ decoded = decode(struct_def, bytearray(b"\x01"),
244
+ enum_def_dict={"Status": status_enum})
245
+
246
+ print(decoded.field_instances[0].value)
247
+ print(decoded.field_instances[0].enum_item)
248
+ ```
249
+
250
+ Output:
251
+
252
+ ```python
253
+ 1
254
+ ('NG', 1)
255
+ ```
256
+
257
+ You can also persist enum definitions by name with
258
+ `save_enum_def_dict` / `load_enum_def_dict`, mirroring `StructDef` dictionaries.
259
+
222
260
  ## Development
223
261
 
224
262
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sltcodec"
3
- version = "1.0.0"
3
+ version = "1.2.0"
4
4
  description = "Decode and encode bytearrays according to struct layout definitions using sltcore."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -105,6 +105,18 @@ def _resolve_size(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
105
105
  return _resolve_info_size(field_def.size, env)
106
106
 
107
107
 
108
+ def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
109
+ """Resolve a byte-swap flag that can be static or expression-based."""
110
+ if isinstance(field_def.byte_swap, str):
111
+ return bool(SltEval(env).eval(field_def.byte_swap))
112
+ return field_def.byte_swap
113
+
114
+
115
+ def _byte_swap_info(info: Info, size: InfoSize) -> Info:
116
+ """Reverse the byte representation of an Info payload."""
117
+ return Info.from_bytes(info.to_bytes[::-1], size, scale=info.scale)
118
+
119
+
108
120
  def _is_padding_field_def(field_def: FieldDef) -> bool:
109
121
  """Check whether a field definition represents padding."""
110
122
  return (field_def.name.startswith("padding[")
@@ -235,6 +247,8 @@ def encode_field(
235
247
  return
236
248
 
237
249
  offset, size, info = prepared
250
+ if _resolve_byte_swap(field_def, env):
251
+ info = _byte_swap_info(info, size)
238
252
  required_bytes = (offset + size).bytes
239
253
  if len(buf) < required_bytes:
240
254
  buf.extend(b"\x00" * (required_bytes - len(buf)))
@@ -341,6 +355,9 @@ def decode_field(
341
355
  if size.byte == 0 and size.bit == 0:
342
356
  return None
343
357
  info = bits_get(data, offset, size, scale=field_def.scale)
358
+ byte_swap = _resolve_byte_swap(field_def, env)
359
+ if byte_swap:
360
+ info = _byte_swap_info(info, size)
344
361
  resolved_type = _resolve_field_type(field_def.type, struct_def_dict, env)
345
362
  resolved_field_def = FieldDef(
346
363
  name=field_def.name,
@@ -351,7 +368,8 @@ def decode_field(
351
368
  repeat=field_def.repeat,
352
369
  description=field_def.description,
353
370
  range_expression=field_def.range_expression,
354
- enum_def=field_def.enum_def,
371
+ enum_def_name=field_def.enum_def_name,
372
+ byte_swap=byte_swap,
355
373
  )
356
374
  if isinstance(resolved_type, StructDef):
357
375
  return FieldInstance(
@@ -104,8 +104,12 @@ 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
- enum_def: EnumDef | None = field(
108
- default=None, metadata={"desc": "The enum definition for the field"})
107
+ enum_def_name: str | None = field(
108
+ default=None,
109
+ metadata={"desc": "The name of the enum definition for the field"})
110
+ byte_swap: bool | str = field(
111
+ default=False,
112
+ metadata={"desc": "Whether to reverse the field byte order"})
109
113
 
110
114
  def split_repeat(self,
111
115
  index: int,
@@ -123,6 +127,8 @@ class FieldDef:
123
127
  split_name)
124
128
  split_range_expression = self._replace_name_in_expression(
125
129
  self.range_expression, self.name, split_name)
130
+ split_byte_swap = self._replace_name_in_expression(
131
+ self.byte_swap, self.name, split_name)
126
132
 
127
133
  return FieldDef(name=split_name,
128
134
  offset=split_offset,
@@ -132,7 +138,8 @@ class FieldDef:
132
138
  repeat=None,
133
139
  description=self.description,
134
140
  range_expression=split_range_expression,
135
- enum_def=self.enum_def)
141
+ enum_def_name=self.enum_def_name,
142
+ byte_swap=split_byte_swap)
136
143
 
137
144
  @staticmethod
138
145
  def _replace_name_in_expression(value: Any, old_name: str,
@@ -178,8 +185,10 @@ class FieldDef:
178
185
  self.description,
179
186
  "range_expression":
180
187
  self.range_expression,
181
- "enum_def":
182
- None if self.enum_def is None else self.enum_def.serialize(),
188
+ "enum_def_name":
189
+ self.enum_def_name,
190
+ "byte_swap":
191
+ self.byte_swap,
183
192
  }
184
193
 
185
194
  def to_json(self) -> str:
@@ -197,7 +206,8 @@ class FieldDef:
197
206
  repeat = data.get("repeat")
198
207
  description = data.get("description")
199
208
  range_expression = data.get("range_expression")
200
- enum_def_data = data.get("enum_def")
209
+ enum_def_name = data.get("enum_def_name")
210
+ byte_swap = data.get("byte_swap", False)
201
211
 
202
212
  def _deserialize_info_like(value: Any) -> Any:
203
213
  if isinstance(value, dict):
@@ -233,14 +243,6 @@ class FieldDef:
233
243
  else:
234
244
  type_value = type_data
235
245
 
236
- if enum_def_data is None:
237
- enum_def = None
238
- elif (isinstance(enum_def_data, dict)
239
- and enum_def_data.get("__type__") == "EnumDef"):
240
- enum_def = EnumDef.deserialize(enum_def_data)
241
- else:
242
- enum_def = None
243
-
244
246
  return cls(
245
247
  name=name,
246
248
  offset=offset,
@@ -250,7 +252,8 @@ class FieldDef:
250
252
  repeat=repeat,
251
253
  description=description,
252
254
  range_expression=range_expression,
253
- enum_def=enum_def,
255
+ enum_def_name=enum_def_name,
256
+ byte_swap=byte_swap,
254
257
  )
255
258
 
256
259
  @classmethod
@@ -269,7 +272,8 @@ class FieldDef:
269
272
  -1 if self.repeat is None else self.repeat,
270
273
  "" if self.description is None else self.description,
271
274
  "" if self.range_expression is None else self.range_expression,
272
- self._sortable_enum_type(self.enum_def),
275
+ "" if self.enum_def_name is None else self.enum_def_name,
276
+ self._sortable_byte_swap(self.byte_swap),
273
277
  )
274
278
 
275
279
  @staticmethod
@@ -287,17 +291,11 @@ class FieldDef:
287
291
  return (1, value)
288
292
 
289
293
  @staticmethod
290
- def _sortable_enum_type(value: EnumDef | None) -> str:
291
- """Build a comparable key for enum definition metadata."""
292
- if value is None:
293
- return ""
294
- return json.dumps(
295
- {
296
- "name": value.name,
297
- "description": value.description,
298
- "values": value.values,
299
- },
300
- sort_keys=True)
294
+ def _sortable_byte_swap(value: bool | str) -> tuple[int, str]:
295
+ """Build a comparable key for a byte-swap flag or expression."""
296
+ if isinstance(value, bool):
297
+ return 0, str(value)
298
+ return 1, value
301
299
 
302
300
 
303
301
  @total_ordering
@@ -351,10 +349,10 @@ class FieldInstance:
351
349
  field_def: FieldDef,
352
350
  enum_def_dict: dict[str, EnumDef] | None) -> EnumDef | None:
353
351
  """Resolve enum definition from field metadata or lookup dictionary."""
354
- if field_def.enum_def is not None:
355
- return field_def.enum_def
356
352
  if not enum_def_dict:
357
353
  return None
354
+ if field_def.enum_def_name is not None:
355
+ return enum_def_dict.get(field_def.enum_def_name)
358
356
  if isinstance(field_def.type, str) and field_def.type in enum_def_dict:
359
357
  return enum_def_dict[field_def.type]
360
358
  return enum_def_dict.get(field_def.name)
@@ -179,7 +179,7 @@ def test_repeated_field_preserves_range_and_enum_metadata():
179
179
  type="unsigned int",
180
180
  repeat=2,
181
181
  range_expression="0 <= value <= 255",
182
- enum_def=enum_def)
182
+ enum_def_name=enum_def.name)
183
183
 
184
184
  encoded = encode(
185
185
  StructInstance(struct_def=StructDef(fields=[field_def]),
@@ -192,8 +192,8 @@ def test_repeated_field_preserves_range_and_enum_metadata():
192
192
  "0 <= value[0] <= 255")
193
193
  assert decoded.field_instances[1].field_def.range_expression == (
194
194
  "0 <= value[1] <= 255")
195
- assert decoded.field_instances[0].field_def.enum_def == enum_def
196
- assert decoded.field_instances[1].field_def.enum_def == enum_def
195
+ assert decoded.field_instances[0].field_def.enum_def_name == enum_def.name
196
+ assert decoded.field_instances[1].field_def.enum_def_name == enum_def.name
197
197
 
198
198
 
199
199
  def test_split_repeat_replaces_name_in_range_expression():
@@ -242,23 +242,86 @@ def test_field_def_to_json_from_json_round_trip():
242
242
  repeat=3,
243
243
  description="A repeated value",
244
244
  range_expression="0 <= value <= 255",
245
- enum_def=enum_def)
245
+ enum_def_name=enum_def.name)
246
246
 
247
247
  payload = field_def.to_json()
248
248
  restored = FieldDef.from_json(payload)
249
249
 
250
250
  assert restored == field_def
251
251
  assert restored.range_expression == "0 <= value <= 255"
252
- assert restored.enum_def == enum_def
253
- assert restored.to_dict()["enum_def"] == {
254
- "__type__": "EnumDef",
255
- "name": "ValueKind",
256
- "description": None,
257
- "values": {
258
- member.name: member.value
259
- for member in ValueKind
260
- },
261
- }
252
+ assert restored.enum_def_name == enum_def.name
253
+ assert restored.to_dict()["enum_def_name"] == "ValueKind"
254
+
255
+
256
+ def test_field_def_byte_swap_json_round_trip():
257
+ """Test that expression-based byte_swap metadata is preserved."""
258
+ field_def = FieldDef(name="value",
259
+ offset=InfoSize(0, 0),
260
+ size=InfoSize(2, 0),
261
+ type="unsigned int",
262
+ byte_swap="kind == 1")
263
+
264
+ restored = FieldDef.from_json(field_def.to_json())
265
+
266
+ assert restored == field_def
267
+ assert restored.byte_swap == "kind == 1"
268
+
269
+
270
+ def test_byte_swap_reverses_bytearray_input_and_output():
271
+ """Test byte_swap reverses bytearray values symmetrically."""
272
+ field_def = FieldDef(name="payload",
273
+ offset=InfoSize(0, 0),
274
+ size=InfoSize(3, 0),
275
+ type="bytearray",
276
+ byte_swap=True)
277
+
278
+ encoded = encode(
279
+ StructInstance(struct_def=StructDef(fields=[field_def]),
280
+ field_instances=[
281
+ FieldInstance(field_def, bytearray(b"\x01\x02\x03"))
282
+ ]),
283
+ bytearray(),
284
+ )
285
+ decoded = decode([field_def], encoded)
286
+
287
+ assert encoded == bytearray(b"\x03\x02\x01")
288
+ assert decoded.field_instances == [
289
+ FieldInstance(field_def, bytearray(b"\x01\x02\x03"))
290
+ ]
291
+
292
+
293
+ def test_byte_swap_expression_uses_previous_field_value():
294
+ """Test byte_swap expressions use values decoded or encoded before it."""
295
+ kind_field = FieldDef(name="kind",
296
+ offset=InfoSize(0, 0),
297
+ size=InfoSize(1, 0),
298
+ type="unsigned int")
299
+ value_field = FieldDef(name="value",
300
+ offset=InfoSize(1, 0),
301
+ size=InfoSize(2, 0),
302
+ type="unsigned int",
303
+ byte_swap="kind == 1")
304
+
305
+ encoded = encode(
306
+ StructInstance(struct_def=StructDef(fields=[kind_field, value_field]),
307
+ field_instances=[
308
+ FieldInstance(kind_field, 1),
309
+ FieldInstance(value_field, 0x1234),
310
+ ]),
311
+ bytearray(),
312
+ )
313
+ decoded = decode([kind_field, value_field], encoded)
314
+
315
+ assert encoded == bytearray(b"\x01\x34\x12")
316
+ assert decoded.field_instances == [
317
+ FieldInstance(kind_field, 1),
318
+ FieldInstance(
319
+ FieldDef(name="value",
320
+ offset=InfoSize(1, 0),
321
+ size=InfoSize(2, 0),
322
+ type="unsigned int",
323
+ byte_swap=True), 0x1234),
324
+ ]
262
325
 
263
326
 
264
327
  def test_enum_def_to_json_from_json_round_trip():
@@ -354,9 +417,10 @@ def test_field_instance_from_value_sets_enum_item_from_field_enum_def():
354
417
  offset=InfoSize(0, 0),
355
418
  size=InfoSize(1, 0),
356
419
  type="unsigned int",
357
- enum_def=enum_def)
420
+ enum_def_name=enum_def.name)
358
421
 
359
- field_instance = FieldInstance.from_value(field_def, 1)
422
+ field_instance = FieldInstance.from_value(
423
+ field_def, 1, enum_def_dict={enum_def.name: enum_def})
360
424
 
361
425
  assert field_instance.enum_item == ("MANUAL", 1)
362
426
 
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "sltcodec"
77
- version = "0.6.0"
77
+ version = "1.1.0"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "sltcalc" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes