sltcodec 2.1.1__tar.gz → 2.3.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: 2.1.1
3
+ Version: 2.3.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
@@ -11,7 +11,7 @@ License-Expression: MIT
11
11
  License-File: LICENSE
12
12
  Keywords: byte,deserialization,layout,serialization,struct
13
13
  Requires-Python: >=3.12
14
- Requires-Dist: sltcalc>=0.1.0
14
+ Requires-Dist: sltcalc>=1.0.0
15
15
  Requires-Dist: sltcore>=1.5.0
16
16
  Description-Content-Type: text/markdown
17
17
 
@@ -77,7 +77,7 @@ The public `PRIMITIVE_TYPES` set contains:
77
77
  {"bool", "signed int", "int", "unsigned int", "float", "bytearray", "bytes"}
78
78
  ```
79
79
 
80
- `FieldDef.type` may also be a nested `StructDef` or an expression string. Expressions can use values from previously processed fields, allowing dynamic types and sizes. Repeated fields, byte swapping, padding, ranges, and enum metadata are supported.
80
+ `FieldDef.type` may also be a nested `StructDef` or an expression string. Expressions can use values from previously processed fields, allowing dynamic types, sizes, and repeat counts. Repeated fields, byte swapping, padding, ranges, and enum metadata are supported.
81
81
 
82
82
  ## Enums And Named Structures
83
83
 
@@ -180,7 +180,7 @@ internal implementation details and are omitted from the public method tables.
180
180
  | `size` | `InfoSize \| str` | Static size or expression. |
181
181
  | `type` | `str \| StructDef` | Primitive, named, or nested field type. |
182
182
  | `scale` | `float` | Numeric scale applied to the field. |
183
- | `repeat` | `int \| None` | Number of repeated field values. |
183
+ | `repeat` | `int \| str \| None` | Number of repeated field values, or an expression that evaluates to the count. |
184
184
  | `description` | `str \| None` | Optional field description. |
185
185
  | `range_expression` | `str \| None` | Optional value-range expression. |
186
186
  | `enum_def_name` | `str \| None` | Name of the associated enum definition. |
@@ -60,7 +60,7 @@ The public `PRIMITIVE_TYPES` set contains:
60
60
  {"bool", "signed int", "int", "unsigned int", "float", "bytearray", "bytes"}
61
61
  ```
62
62
 
63
- `FieldDef.type` may also be a nested `StructDef` or an expression string. Expressions can use values from previously processed fields, allowing dynamic types and sizes. Repeated fields, byte swapping, padding, ranges, and enum metadata are supported.
63
+ `FieldDef.type` may also be a nested `StructDef` or an expression string. Expressions can use values from previously processed fields, allowing dynamic types, sizes, and repeat counts. Repeated fields, byte swapping, padding, ranges, and enum metadata are supported.
64
64
 
65
65
  ## Enums And Named Structures
66
66
 
@@ -163,7 +163,7 @@ internal implementation details and are omitted from the public method tables.
163
163
  | `size` | `InfoSize \| str` | Static size or expression. |
164
164
  | `type` | `str \| StructDef` | Primitive, named, or nested field type. |
165
165
  | `scale` | `float` | Numeric scale applied to the field. |
166
- | `repeat` | `int \| None` | Number of repeated field values. |
166
+ | `repeat` | `int \| str \| None` | Number of repeated field values, or an expression that evaluates to the count. |
167
167
  | `description` | `str \| None` | Optional field description. |
168
168
  | `range_expression` | `str \| None` | Optional value-range expression. |
169
169
  | `enum_def_name` | `str \| None` | Name of the associated enum definition. |
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "sltcodec"
3
- version = "2.1.1"
3
+ version = "2.3.0"
4
4
  description = "Decode and encode bytearrays according to struct layout definitions using sltcore."
5
5
  readme = "README_pypi.md"
6
6
  requires-python = ">=3.12"
7
7
  dependencies = [
8
- "sltcalc>=0.1.0",
8
+ "sltcalc>=1.0.0",
9
9
  "sltcore>=1.5.0",
10
10
  ]
11
11
  license = "MIT"
@@ -136,20 +136,12 @@ def _resolve_info_size(value: InfoSize | str, env: dict[str, Any]) -> InfoSize:
136
136
  if isinstance(value, str):
137
137
  stleval = SltEval(env)
138
138
  resolved_byte = stleval.eval(value)
139
+ if isinstance(resolved_byte, InfoSize):
140
+ return resolved_byte
139
141
  return InfoSize(resolved_byte, 0)
140
142
  return value
141
143
 
142
144
 
143
- def _resolve_offset(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
144
- """Resolve a field offset that can be static or expression-based."""
145
- return _resolve_info_size(field_def.offset, env)
146
-
147
-
148
- def _resolve_size(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
149
- """Resolve a field size that can be static or expression-based."""
150
- return _resolve_info_size(field_def.size, env)
151
-
152
-
153
145
  def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
154
146
  """Resolve a byte-swap flag that can be static or expression-based."""
155
147
  if isinstance(field_def.byte_swap, str):
@@ -157,9 +149,11 @@ def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
157
149
  return field_def.byte_swap
158
150
 
159
151
 
160
- def _byte_swap_info(info: Info, size: InfoSize) -> Info:
161
- """Reverse the byte representation of an Info payload."""
162
- return Info.from_bytes(info.to_bytes[::-1], size, scale=info.scale)
152
+ def _resolve_repeat(value: int | str | None, env: dict[str, Any]) -> int | None:
153
+ """Resolve a static or expression-based repeat count."""
154
+ if isinstance(value, str):
155
+ return int(SltEval(env).eval(value))
156
+ return value
163
157
 
164
158
 
165
159
  def _is_padding_field_def(field_def: FieldDef) -> bool:
@@ -168,16 +162,6 @@ def _is_padding_field_def(field_def: FieldDef) -> bool:
168
162
  and field_def.type in ["bytes", "bytearray"])
169
163
 
170
164
 
171
- def _info_size_to_bits(info_size: InfoSize) -> int:
172
- """Convert InfoSize to an absolute bit count."""
173
- return info_size.byte * 8 + info_size.bit
174
-
175
-
176
- def _info_size_from_bits(total_bits: int) -> InfoSize:
177
- """Create InfoSize from an absolute bit count."""
178
- return InfoSize(total_bits >> 3, total_bits & 0x7)
179
-
180
-
181
165
  def _validate_padding_alignment_bits(padding_alignment_bits: int) -> None:
182
166
  """Validate padding alignment as a positive power-of-two bit size."""
183
167
  if (isinstance(padding_alignment_bits, bool)
@@ -237,8 +221,8 @@ def _prepare_field_info(
237
221
  padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
238
222
  ) -> tuple[InfoSize, InfoSize, Info] | None:
239
223
  """Resolve a field definition into an offset, size, and info payload."""
240
- offset = _resolve_offset(field_def, env)
241
- size = _resolve_size(field_def, env)
224
+ offset = _resolve_info_size(field_def.offset, env)
225
+ size = _resolve_info_size(field_def.size, env)
242
226
  if size.byte == 0 and size.bit == 0:
243
227
  return None
244
228
 
@@ -305,7 +289,7 @@ def encode_field(
305
289
 
306
290
  offset, size, info = prepared
307
291
  if _resolve_byte_swap(field_def, env):
308
- info = _byte_swap_info(info, size)
292
+ info = info.byte_swap
309
293
  required_bytes = (offset + size).bytes
310
294
  if len(buf) < required_bytes:
311
295
  buf.extend(b"\x00" * (required_bytes - len(buf)))
@@ -351,21 +335,22 @@ def encode(
351
335
 
352
336
  for field_value in struct_instance.field_instances:
353
337
  field_def = field_value.field_def
338
+ repeat = _resolve_repeat(field_def.repeat, env)
354
339
  if _is_padding_field_def(field_def):
355
340
  has_padding = True
356
341
  value = field_value.value
357
342
  if _is_padding_field_def(field_def):
358
- padding_size = _resolve_size(field_def, env)
343
+ padding_size = _resolve_info_size(field_def.size, env)
359
344
  value = bytearray(padding_size.bytes)
360
- if field_def.repeat is not None and field_def.repeat > 1:
361
- current_offset = _resolve_offset(field_def, env)
345
+ if repeat is not None and repeat > 1:
346
+ current_offset = _resolve_info_size(field_def.offset, env)
362
347
  values = list(value)
363
348
 
364
- for i in range(field_def.repeat):
349
+ for i in range(repeat):
365
350
  field_def_repeat = _repeated_field_def(field_def, i,
366
351
  current_offset,
367
352
  field_def.size)
368
- resolved_size = _resolve_size(field_def_repeat, env)
353
+ resolved_size = _resolve_info_size(field_def_repeat.size, env)
369
354
  encode_field(field_def_repeat, values[i], buf, env, type_dict,
370
355
  padding_alignment_bits)
371
356
  env[field_def_repeat.name] = values[i]
@@ -410,14 +395,14 @@ def decode_field(
410
395
  """
411
396
  if env is None:
412
397
  env = {}
413
- offset = _resolve_offset(field_def, env)
414
- size = _resolve_size(field_def, env)
398
+ offset = _resolve_info_size(field_def.offset, env)
399
+ size = _resolve_info_size(field_def.size, env)
415
400
  if size.byte == 0 and size.bit == 0:
416
401
  return None
417
402
  info = bits_get(data, offset, size, scale=field_def.scale)
418
403
  byte_swap = _resolve_byte_swap(field_def, env)
419
404
  if byte_swap:
420
- info = _byte_swap_info(info, size)
405
+ info = info.byte_swap
421
406
  resolved_type = _resolve_field_type(field_def.type, type_dict, env)
422
407
  nested_value = None
423
408
  if isinstance(resolved_type, StructDef):
@@ -526,22 +511,21 @@ def decode(
526
511
 
527
512
  def append_padding_until(target_offset: InfoSize) -> None:
528
513
  nonlocal current_position, padding_index
529
- start_bits = _info_size_to_bits(current_position)
530
- end_bits = _info_size_to_bits(target_offset)
514
+ start_bits = current_position.bits
515
+ end_bits = target_offset.bits
531
516
  if end_bits <= start_bits:
532
517
  return
533
518
 
534
519
  chunk_start_bits = start_bits
535
520
  while chunk_start_bits < end_bits:
536
- chunk_start = _info_size_from_bits(chunk_start_bits)
537
- bits_to_boundary = _info_size_to_bits(
538
- chunk_start.align_to(padding_alignment_bits))
521
+ chunk_start = InfoSize(0, chunk_start_bits)
522
+ bits_to_boundary = chunk_start.align_to(padding_alignment_bits).bits
539
523
  chunk_size_bits = bits_to_boundary or padding_alignment_bits
540
524
  chunk_end_bits = min(end_bits, chunk_start_bits + chunk_size_bits)
541
525
  padding_field_def = FieldDef(
542
526
  name=f"padding[{padding_index}]",
543
- offset=_info_size_from_bits(chunk_start_bits),
544
- size=_info_size_from_bits(chunk_end_bits - chunk_start_bits),
527
+ offset=InfoSize(0, chunk_start_bits),
528
+ size=InfoSize(0, chunk_end_bits - chunk_start_bits),
545
529
  type="bytes",
546
530
  description="Auto-generated padding",
547
531
  )
@@ -554,9 +538,10 @@ def decode(
554
538
  current_position = target_offset
555
539
 
556
540
  for field_def in _as_field_defs(struct_def_obj):
541
+ repeat = _resolve_repeat(field_def.repeat, env)
557
542
  # Handle non-repeated fields
558
- if field_def.repeat is None or field_def.repeat <= 1:
559
- resolved_offset = _resolve_offset(field_def, env)
543
+ if repeat is None or repeat <= 1:
544
+ resolved_offset = _resolve_info_size(field_def.offset, env)
560
545
  append_padding_until(resolved_offset)
561
546
  field_instance = decode_field(field_def, data, env, type_dict,
562
547
  padding_alignment_bits)
@@ -564,12 +549,12 @@ def decode(
564
549
  env[field_instance.field_def.name] = field_instance.value
565
550
  result.append_field_instance(field_instance)
566
551
  current_position = (resolved_offset +
567
- _resolve_size(field_def, env))
552
+ _resolve_info_size(field_def.size, env))
568
553
  continue
569
554
  # Handle repeated fields
570
- current_offset = _resolve_offset(field_def, env)
555
+ current_offset = _resolve_info_size(field_def.offset, env)
571
556
  append_padding_until(current_offset)
572
- for i in range(field_def.repeat):
557
+ for i in range(repeat):
573
558
  field_def_repeat = _repeated_field_def(field_def, i, current_offset,
574
559
  field_def.size)
575
560
  field_instance = decode_field(field_def_repeat, data, env,
@@ -579,7 +564,7 @@ def decode(
579
564
  result.append_field_instance(field_instance)
580
565
  actual_size = field_instance.field_def.size
581
566
  else:
582
- actual_size = _resolve_size(field_def_repeat, env)
567
+ actual_size = _resolve_info_size(field_def_repeat.size, env)
583
568
  if isinstance(current_offset, InfoSize) and isinstance(
584
569
  actual_size, InfoSize):
585
570
  current_offset += actual_size
@@ -98,7 +98,7 @@ class FieldDef:
98
98
  metadata={"desc": "The type of the field"})
99
99
  scale: float = field(default=1.0,
100
100
  metadata={"desc": "The scale of the field"})
101
- repeat: int | None = field(
101
+ repeat: int | str | None = field(
102
102
  default=None, metadata={"desc": "The repeat count of the field"})
103
103
  description: str | None = field(
104
104
  default=None, metadata={"desc": "The description of the field"})
@@ -231,7 +231,7 @@ class FieldDef:
231
231
  self.name,
232
232
  self._sortable_type(self.type),
233
233
  self.scale,
234
- -1 if self.repeat is None else self.repeat,
234
+ self._sortable_repeat(self.repeat),
235
235
  "" if self.description is None else self.description,
236
236
  "" if self.range_expression is None else self.range_expression,
237
237
  "" if self.enum_def_name is None else self.enum_def_name,
@@ -245,6 +245,15 @@ class FieldDef:
245
245
  return (0, value.byte, value.bit)
246
246
  return (1, value)
247
247
 
248
+ @staticmethod
249
+ def _sortable_repeat(value: int | str | None) -> tuple[int, int | str]:
250
+ """Build a comparable key for static or expression repeat counts."""
251
+ if value is None:
252
+ return 0, -1
253
+ if isinstance(value, int):
254
+ return 1, value
255
+ return 2, value
256
+
248
257
  @staticmethod
249
258
  def _sortable_type(value: str | "StructDef") -> tuple[Any, ...]:
250
259
  """Build a comparable key for primitive or nested field types."""
@@ -7,7 +7,7 @@ from sltcore import InfoSize
7
7
  from sltcodec import (PRIMITIVE_TYPES, EnumDef, FieldDef, FieldInstance,
8
8
  StructDef, StructInstance, StructLayout, TypeDict, decode,
9
9
  encode)
10
- from sltcodec.codec import decode_field
10
+ from sltcodec.codec import _resolve_info_size, decode_field
11
11
 
12
12
 
13
13
  def layout_for(struct_def):
@@ -166,6 +166,38 @@ def test_encode_layout_handles_repeat():
166
166
  ]
167
167
 
168
168
 
169
+ def test_expression_repeat_uses_previous_field_value():
170
+ """Test that repeat expressions are evaluated against the field env."""
171
+ seed_field = FieldDef(name="count",
172
+ offset=InfoSize(0, 0),
173
+ size=InfoSize(1, 0),
174
+ type="unsigned int")
175
+ repeated_field = FieldDef(name="value",
176
+ offset=InfoSize(1, 0),
177
+ size=InfoSize(1, 0),
178
+ type="unsigned int",
179
+ repeat="count + 1")
180
+ struct_def = StructDef(fields=[seed_field, repeated_field])
181
+
182
+ encoded = encode(
183
+ layout_for(struct_def),
184
+ StructInstance(
185
+ struct_def=struct_def,
186
+ field_instances=[
187
+ FieldInstance(seed_field, 2),
188
+ FieldInstance(repeated_field, [5, 6, 7]),
189
+ ],
190
+ ),
191
+ bytearray(),
192
+ )
193
+
194
+ assert encoded == bytearray(b"\x02\x05\x06\x07")
195
+ decoded = decode(layout_for(struct_def), encoded)
196
+
197
+ assert [instance.value
198
+ for instance in decoded.field_instances] == [2, 5, 6, 7]
199
+
200
+
169
201
  def test_encode_recurses_for_nested_field_types():
170
202
  """Test that encoding a field with a nested field type works correctly."""
171
203
  child_field_defs = [
@@ -608,6 +640,15 @@ def test_decode_field_sets_actual_size_on_field_instance():
608
640
  assert decoded.field_def.size == InfoSize(1, 0)
609
641
 
610
642
 
643
+ def test_resolve_info_size_returns_expression_info_size_unchanged():
644
+ """Test that expressions resolving to InfoSize preserve the result."""
645
+ info_size = InfoSize(2, 3)
646
+
647
+ resolved = _resolve_info_size("field_size", {"field_size": info_size})
648
+
649
+ assert resolved is info_size
650
+
651
+
611
652
  def test_decode_struct_instance_size_matches_decoded_layout():
612
653
  """Test that decode reports the complete decoded layout size."""
613
654
  field_def = FieldDef(name="value",
@@ -39,7 +39,7 @@ def test_field_def_to_json_from_json_round_trip():
39
39
  size=InfoSize(1, 0),
40
40
  type="unsigned int",
41
41
  scale=2.0,
42
- repeat=3,
42
+ repeat="count + 1",
43
43
  description="A repeated value",
44
44
  range_expression="0 <= value <= 255",
45
45
  enum_def_name=enum_def.name)
@@ -48,6 +48,7 @@ def test_field_def_to_json_from_json_round_trip():
48
48
  restored = FieldDef.from_json(payload)
49
49
 
50
50
  assert restored == field_def
51
+ assert restored.repeat == "count + 1"
51
52
  assert restored.range_expression == "0 <= value <= 255"
52
53
  assert restored.enum_def_name == enum_def.name
53
54
  assert restored.to_dict()["enum_def_name"] == "ValueKind"
@@ -65,16 +65,16 @@ wheels = [
65
65
 
66
66
  [[package]]
67
67
  name = "sltcalc"
68
- version = "0.1.0"
68
+ version = "1.0.0"
69
69
  source = { registry = "https://pypi.org/simple" }
70
- sdist = { url = "https://files.pythonhosted.org/packages/a2/16/2a988c23ac06d05c82e5555e0fb221a8e00786539ebe2826365dcb4f3abe/sltcalc-0.1.0.tar.gz", hash = "sha256:599dc33cf91d18dd257489989065debf5d20d28c299d95f586b371cc86af1b47", size = 10343, upload-time = "2026-08-01T15:06:22.695Z" }
70
+ sdist = { url = "https://files.pythonhosted.org/packages/b5/01/19db81d0e1f1330ab884abf282569b3f05ffe1f2c7b8aff910125295d4be/sltcalc-1.0.0.tar.gz", hash = "sha256:968053bbecc85c30caf5ddfd48962e409c7844ccfc441418d029a4f743030350", size = 10499, upload-time = "2026-08-18T14:38:14.052Z" }
71
71
  wheels = [
72
- { url = "https://files.pythonhosted.org/packages/8a/45/c6f7d8074f1fb743e3902d71d8c028e9edeb032834639fdb72df213fdd11/sltcalc-0.1.0-py3-none-any.whl", hash = "sha256:20f06501a583aec62c670e48b4d21eba02a42997461bd8f85640055092d710fa", size = 4783, upload-time = "2026-08-01T15:06:21.411Z" },
72
+ { url = "https://files.pythonhosted.org/packages/da/77/6d03b5c160ffe42bee2fdabdd46521033794fb02e8975b4da7a1f1c641de/sltcalc-1.0.0-py3-none-any.whl", hash = "sha256:12531fef9633b25e6743c4a41873971c83dffb763590f42e2f1f78accea08088", size = 4824, upload-time = "2026-08-18T14:38:13.247Z" },
73
73
  ]
74
74
 
75
75
  [[package]]
76
76
  name = "sltcodec"
77
- version = "2.1.0"
77
+ version = "2.2.0"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "sltcalc" },
@@ -88,7 +88,7 @@ dev = [
88
88
 
89
89
  [package.metadata]
90
90
  requires-dist = [
91
- { name = "sltcalc", specifier = ">=0.1.0" },
91
+ { name = "sltcalc", specifier = ">=1.0.0" },
92
92
  { name = "sltcore", specifier = ">=1.5.0" },
93
93
  ]
94
94
 
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