sltcodec 0.4.0__tar.gz → 0.6.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: 0.4.0
3
+ Version: 0.6.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
@@ -25,6 +25,17 @@ sltcodec is a small package for decoding and encoding bytearrays according to st
25
25
  pip install sltcodec
26
26
  ```
27
27
 
28
+ ## Primitive Type Set
29
+
30
+ `PRIMITIVE_TYPES` is the public set of built-in field types understood by the codec.
31
+
32
+ ```python
33
+ from sltcodec import PRIMITIVE_TYPES
34
+
35
+ print(PRIMITIVE_TYPES)
36
+ # {'bool', 'signed int', 'int', 'unsigned int', 'float', 'bytearray', 'bytes'}
37
+ ```
38
+
28
39
  ## Quick Example
29
40
 
30
41
  `FieldDef.description` is an optional human-readable note that can be attached to each field definition.
@@ -8,6 +8,17 @@ sltcodec is a small package for decoding and encoding bytearrays according to st
8
8
  pip install sltcodec
9
9
  ```
10
10
 
11
+ ## Primitive Type Set
12
+
13
+ `PRIMITIVE_TYPES` is the public set of built-in field types understood by the codec.
14
+
15
+ ```python
16
+ from sltcodec import PRIMITIVE_TYPES
17
+
18
+ print(PRIMITIVE_TYPES)
19
+ # {'bool', 'signed int', 'int', 'unsigned int', 'float', 'bytearray', 'bytes'}
20
+ ```
21
+
11
22
  ## Quick Example
12
23
 
13
24
  `FieldDef.description` is an optional human-readable note that can be attached to each field definition.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sltcodec"
3
- version = "0.4.0"
3
+ version = "0.6.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"
@@ -1,8 +1,9 @@
1
- from .codec import (decode, encode, encode_field, load_struct_def_dict,
2
- save_struct_def_dict)
1
+ from .codec import (PRIMITIVE_TYPES, decode, encode, encode_field,
2
+ load_struct_def_dict, save_struct_def_dict)
3
3
  from .types import EnumDef, FieldDef, FieldInstance, StructDef, StructInstance
4
4
 
5
5
  __all__ = [
6
+ "PRIMITIVE_TYPES",
6
7
  "EnumDef",
7
8
  "FieldDef",
8
9
  "FieldInstance",
@@ -10,7 +10,7 @@ from sltcore import Info, InfoSize, bits_get, bits_set
10
10
 
11
11
  from .types import EnumDef, FieldDef, FieldInstance, StructDef, StructInstance
12
12
 
13
- _PRIMITIVE_TYPES = {
13
+ PRIMITIVE_TYPES = {
14
14
  "bool",
15
15
  "signed int",
16
16
  "int",
@@ -20,6 +20,9 @@ _PRIMITIVE_TYPES = {
20
20
  "bytes",
21
21
  }
22
22
 
23
+ # Backward-compatible alias for internal usage.
24
+ _PRIMITIVE_TYPES = PRIMITIVE_TYPES
25
+
23
26
  _DEFAULT_PADDING_ALIGNMENT_BITS = 32
24
27
 
25
28
 
@@ -319,34 +322,45 @@ def decode_field(
319
322
  return None
320
323
  info = bits_get(data, offset, size, scale=field_def.scale)
321
324
  resolved_type = _resolve_field_type(field_def.type, struct_def_dict, env)
325
+ resolved_field_def = FieldDef(
326
+ name=field_def.name,
327
+ offset=offset,
328
+ size=size,
329
+ type=resolved_type,
330
+ scale=field_def.scale,
331
+ repeat=field_def.repeat,
332
+ description=field_def.description,
333
+ range_expression=field_def.range_expression,
334
+ enum_def=field_def.enum_def,
335
+ )
322
336
  if isinstance(resolved_type, StructDef):
323
337
  return FieldInstance(
324
- field_def=field_def,
338
+ field_def=resolved_field_def,
325
339
  value=decode(resolved_type, bytearray(info.to_bytes),
326
340
  struct_def_dict, enum_def_dict,
327
341
  padding_alignment_bits),
328
342
  )
329
343
  if resolved_type == "bool":
330
- return FieldInstance.from_value(field_def,
344
+ return FieldInstance.from_value(resolved_field_def,
331
345
  info.to_bool,
332
346
  enum_def_dict=enum_def_dict)
333
347
  if resolved_type == "signed int":
334
- return FieldInstance.from_value(field_def,
348
+ return FieldInstance.from_value(resolved_field_def,
335
349
  info.to_signed_int,
336
350
  enum_def_dict=enum_def_dict)
337
351
  if resolved_type in ["int", "unsigned int"]:
338
- return FieldInstance.from_value(field_def,
352
+ return FieldInstance.from_value(resolved_field_def,
339
353
  info.to_unsigned_int,
340
354
  enum_def_dict=enum_def_dict)
341
355
  if resolved_type == "float":
342
- return FieldInstance.from_value(field_def,
356
+ return FieldInstance.from_value(resolved_field_def,
343
357
  info.to_float,
344
358
  enum_def_dict=enum_def_dict)
345
359
  if resolved_type in ["bytearray", "bytes"]:
346
- return FieldInstance.from_value(field_def,
360
+ return FieldInstance.from_value(resolved_field_def,
347
361
  info.to_bytes,
348
362
  enum_def_dict=enum_def_dict)
349
- return FieldInstance.from_value(field_def,
363
+ return FieldInstance.from_value(resolved_field_def,
350
364
  info.raw_value,
351
365
  enum_def_dict=enum_def_dict)
352
366
 
@@ -4,12 +4,14 @@ from __future__ import annotations
4
4
  import json
5
5
  import re
6
6
  from dataclasses import dataclass, field
7
+ from functools import total_ordering
7
8
  from typing import Any
8
9
 
9
10
  from sltcalc import SltEval
10
11
  from sltcore import Info, InfoSize
11
12
 
12
13
 
14
+ @total_ordering
13
15
  @dataclass(frozen=True)
14
16
  class EnumDef:
15
17
  """An enumeration definition."""
@@ -67,7 +69,22 @@ class EnumDef:
67
69
  """Create an enum definition from a JSON string."""
68
70
  return cls.from_dict(json.loads(data))
69
71
 
72
+ def __lt__(self, other: object) -> bool:
73
+ """Compare enum definitions using a stable serialized sort key."""
74
+ if not isinstance(other, EnumDef):
75
+ return NotImplemented
76
+ return self._sort_key() < other._sort_key()
77
+
78
+ def _sort_key(self) -> tuple[Any, ...]:
79
+ """Build a stable comparison key for ordering enum definitions."""
80
+ return (
81
+ self.name,
82
+ "" if self.description is None else self.description,
83
+ json.dumps(self.values, sort_keys=True),
84
+ )
70
85
 
86
+
87
+ @total_ordering
71
88
  @dataclass(frozen=True)
72
89
  class FieldDef:
73
90
  """A field in a structured layout."""
@@ -283,6 +300,7 @@ class FieldDef:
283
300
  sort_keys=True)
284
301
 
285
302
 
303
+ @total_ordering
286
304
  @dataclass(frozen=True)
287
305
  class FieldInstance:
288
306
  """A decoded/encodable field value with its field definition."""
@@ -355,6 +373,7 @@ class FieldInstance:
355
373
  return None
356
374
 
357
375
 
376
+ @total_ordering
358
377
  @dataclass(frozen=True)
359
378
  class StructDef:
360
379
  """A structured layout definition that groups multiple fields."""
@@ -369,6 +388,21 @@ class StructDef:
369
388
  metadata={"desc": "The fields of the structure"},
370
389
  )
371
390
 
391
+ def __lt__(self, other: object) -> bool:
392
+ """Compare structure definitions using a stable serialized sort key."""
393
+ if not isinstance(other, StructDef):
394
+ return NotImplemented
395
+ return self._sort_key() < other._sort_key()
396
+
397
+ def _sort_key(self) -> tuple[Any, ...]:
398
+ """Build a stable comparison key for ordering structure definitions."""
399
+ return (
400
+ self.name,
401
+ "" if self.description is None else self.description,
402
+ json.dumps([field.to_dict() for field in self.fields],
403
+ sort_keys=True),
404
+ )
405
+
372
406
  def to_dict(self) -> dict[str, Any]:
373
407
  """Convert this structure definition to JSON-serializable data."""
374
408
  return {
@@ -402,6 +436,7 @@ class StructDef:
402
436
  return cls.from_dict(json.loads(data))
403
437
 
404
438
 
439
+ @total_ordering
405
440
  @dataclass
406
441
  class StructInstance:
407
442
  """A decoded/encodable structure instance."""
@@ -454,6 +489,23 @@ class StructInstance:
454
489
  """Return one field instance by index."""
455
490
  return self.field_instances[index]
456
491
 
492
+ def __lt__(self, other: object) -> bool:
493
+ """Compare structure instances using a stable serialized sort key."""
494
+ if not isinstance(other, StructInstance):
495
+ return NotImplemented
496
+ return self._sort_key() < other._sort_key()
497
+
498
+ def _sort_key(self) -> tuple[Any, ...]:
499
+ """Build a stable comparison key for ordering structure instances."""
500
+ return (
501
+ self.struct_def._sort_key(),
502
+ json.dumps([
503
+ field_instance.to_dict()
504
+ for field_instance in self.field_instances
505
+ ],
506
+ sort_keys=True),
507
+ )
508
+
457
509
  def _sort_field_instances(self) -> None:
458
510
  """Keep field instances sorted by FieldDef order."""
459
511
  self.field_instances.sort()
@@ -466,14 +518,25 @@ class StructInstance:
466
518
  ]
467
519
  non_padding_instances.sort()
468
520
 
521
+ has_unresolved_layout = any(
522
+ not isinstance(field_instance.field_def.offset, InfoSize)
523
+ or not isinstance(field_instance.field_def.size, InfoSize)
524
+ for field_instance in non_padding_instances)
525
+ if has_unresolved_layout:
526
+ # Layout expressions are resolved later in codec paths.
527
+ self.field_instances = non_padding_instances
528
+ self._sort_field_instances()
529
+ self._update_size()
530
+ return
531
+
469
532
  rebuilt_instances: list[FieldInstance] = []
470
533
  current_offset = InfoSize(0, 0)
471
534
  padding_index = 0
472
535
 
473
536
  for field_instance in non_padding_instances:
474
537
  field_def = field_instance.field_def
475
- field_offset = self._resolve_field_offset(field_def)
476
- field_size = self._resolve_field_size(field_def)
538
+ field_offset = field_def.offset
539
+ field_size = field_def.size
477
540
  if field_offset > current_offset:
478
541
  gap_size = field_offset - current_offset
479
542
  if gap_size.byte > 0 or gap_size.bit > 0:
@@ -521,25 +584,15 @@ class StructInstance:
521
584
  max_end_offset = InfoSize(0, 0)
522
585
  for field_instance in self.field_instances:
523
586
  field_def = field_instance.field_def
524
- field_offset = self._resolve_field_offset(field_def)
525
- field_size = self._resolve_field_size(field_def)
587
+ if not isinstance(field_def.offset, InfoSize):
588
+ continue
589
+ if not isinstance(field_def.size, InfoSize):
590
+ continue
591
+ field_offset = field_def.offset
592
+ field_size = field_def.size
526
593
  field_end = field_offset + field_size
527
594
  if field_end > max_end_offset:
528
595
  max_end_offset = field_end
529
596
 
530
597
  if max_end_offset > self.size:
531
598
  self.size = max_end_offset
532
-
533
- @staticmethod
534
- def _resolve_field_offset(field_def: FieldDef) -> InfoSize:
535
- """Resolve field offsets to InfoSize values when possible."""
536
- if isinstance(field_def.offset, InfoSize):
537
- return field_def.offset
538
- return InfoSize(0, 0)
539
-
540
- @staticmethod
541
- def _resolve_field_size(field_def: FieldDef) -> InfoSize:
542
- """Resolve field sizes to InfoSize values when possible."""
543
- if isinstance(field_def.size, InfoSize):
544
- return field_def.size
545
- return InfoSize(0, 0)
@@ -5,9 +5,9 @@ from pathlib import Path
5
5
  import pytest
6
6
  from sltcore import InfoSize
7
7
 
8
- from sltcodec import (EnumDef, FieldDef, FieldInstance, StructDef,
9
- StructInstance, decode, encode, load_struct_def_dict,
10
- save_struct_def_dict)
8
+ from sltcodec import (PRIMITIVE_TYPES, EnumDef, FieldDef, FieldInstance,
9
+ StructDef, StructInstance, decode, encode,
10
+ load_struct_def_dict, save_struct_def_dict)
11
11
  from sltcodec.codec import decode_field
12
12
 
13
13
 
@@ -17,6 +17,19 @@ class ValueKind(Enum):
17
17
  B = 2
18
18
 
19
19
 
20
+ def test_primitive_types_are_exposed_at_package_root():
21
+ """Test that primitive types are exposed for external consumers."""
22
+ assert PRIMITIVE_TYPES == {
23
+ "bool",
24
+ "signed int",
25
+ "int",
26
+ "unsigned int",
27
+ "float",
28
+ "bytearray",
29
+ "bytes",
30
+ }
31
+
32
+
20
33
  def test_encode_and_decode_round_trip():
21
34
  """Test that encoding and then decoding returns the original data."""
22
35
  struct_def = [
@@ -475,10 +488,11 @@ def test_type_expression_uses_previous_field_value():
475
488
  bytearray(),
476
489
  )
477
490
  decoded_int = decode(struct_def, encoded_int)
478
- assert decoded_int.field_instances == [
479
- FieldInstance(struct_def[0], 1),
480
- FieldInstance(struct_def[1], 7),
481
- ]
491
+ assert decoded_int.field_instances[0] == FieldInstance(struct_def[0], 1)
492
+ assert decoded_int.field_instances[1].field_def.offset == InfoSize(1, 0)
493
+ assert decoded_int.field_instances[1].field_def.size == InfoSize(1, 0)
494
+ assert decoded_int.field_instances[1].field_def.type == "int"
495
+ assert decoded_int.field_instances[1].value == 7
482
496
 
483
497
  encoded_float = encode(
484
498
  StructInstance(struct_def=StructDef(fields=struct_def),
@@ -490,7 +504,9 @@ def test_type_expression_uses_previous_field_value():
490
504
  )
491
505
  decoded_float = decode(struct_def, encoded_float)
492
506
  assert decoded_float.field_instances[0] == FieldInstance(struct_def[0], 2)
493
- assert decoded_float.field_instances[1].field_def == struct_def[1]
507
+ assert decoded_float.field_instances[1].field_def.offset == InfoSize(1, 0)
508
+ assert decoded_float.field_instances[1].field_def.size == InfoSize(4, 0)
509
+ assert decoded_float.field_instances[1].field_def.type == "float"
494
510
  assert decoded_float.field_instances[1].value == 1.5
495
511
 
496
512
 
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "sltcodec"
77
- version = "0.3.0"
77
+ version = "0.4.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