sltcodec 2.4.0__tar.gz → 2.5.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.4.0
3
+ Version: 2.5.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
@@ -63,6 +63,26 @@ encoded = encode(layout, instance, bytearray())
63
63
  decoded = decode(layout, encoded)
64
64
  ```
65
65
 
66
+ ## Progress Callbacks
67
+
68
+ `encode` and `decode` accept an optional `progress_callback` argument. The
69
+ callback is typed as `ProgressCallback` and receives a `float` progress value
70
+ from `0.0` to `1.0`.
71
+
72
+ Progress is calculated for top-level fields as `(offset + size) / total_size`.
73
+ Nested structure encode/decode calls pass `None` internally, so recursive
74
+ fields do not emit additional callback events.
75
+
76
+ ```python
77
+ from sltcodec import ProgressCallback, decode
78
+
79
+ def update_progress(progress: float) -> None:
80
+ progress_bar.value = progress
81
+
82
+ progress_callback: ProgressCallback = update_progress
83
+ decoded = decode(layout, encoded, progress_callback=progress_callback)
84
+ ```
85
+
66
86
  `decode` returns a `StructInstance` whose `size` is the actual end of the
67
87
  decoded layout. `decode_field` stores the actual decoded size in the returned
68
88
  `FieldInstance.field_def.size`. For nested `StructDef` fields, that size comes
@@ -138,6 +158,7 @@ The following symbols are exported by `sltcodec.__all__` in this order:
138
158
  | Symbol | Description |
139
159
  | --- | --- |
140
160
  | `PRIMITIVE_TYPES` | Set of built-in field type names supported by the codec. |
161
+ | `ProgressCallback` | Callable type alias for encode/decode progress updates. |
141
162
  | `EnumDef` | Immutable definition of an enumeration. |
142
163
  | `EnumDict` | Dictionary-like container for `EnumDef` objects. |
143
164
  | `FieldDef` | Immutable definition of one structured field. |
@@ -310,15 +331,16 @@ beginning with `_` are internal helpers and are not part of the public API.
310
331
  | Definition | Signature | Description |
311
332
  | --- | --- | --- |
312
333
  | `PRIMITIVE_TYPES` | `set[str]` | Built-in field type names. |
334
+ | `ProgressCallback` | `Callable[[float], None]` | Progress callback type for top-level encode/decode updates. |
313
335
  | `save_struct_layout` | `(struct_layout, path) -> None` | Save a layout to a JSON file. |
314
336
  | `load_struct_layout` | `(path) -> StructLayout` | Load a layout from a JSON file. |
315
337
  | `save_struct_def_dict` | `(path, struct_def_dict) -> None` | Save a structure-definition dictionary through the layout format. |
316
338
  | `load_struct_def_dict` | `(path) -> dict[str, StructDef]` | Load a structure-definition dictionary. |
317
339
  | `save_enum_def_dict` | `(path, enum_def_dict) -> None` | Save an enum-definition dictionary through the layout format. |
318
340
  | `load_enum_def_dict` | `(path) -> dict[str, EnumDef]` | Load an enum-definition dictionary. |
319
- | `encode` | `(struct_layout, struct_instance, buf, padding_alignment_bits=32) -> bytearray` | Encode a complete structure. |
341
+ | `encode` | `(struct_layout, struct_instance, buf, padding_alignment_bits=32, progress_callback=None) -> bytearray` | Encode a complete structure. |
320
342
  | `decode_field` | `(field_def, data, env=None, type_dict=None, padding_alignment_bits=32) -> FieldInstance \| None` | Decode one field. |
321
- | `decode` | `(struct_layout, data, padding_alignment_bits=32) -> StructInstance` | Decode a complete structure. |
343
+ | `decode` | `(struct_layout, data, padding_alignment_bits=32, progress_callback=None) -> StructInstance` | Decode a complete structure. |
322
344
 
323
345
  `save_struct_def_dict`, `load_struct_def_dict`, `save_enum_def_dict`, and
324
346
  `load_enum_def_dict` are available from `sltcodec.codec` for dictionary-level
@@ -57,6 +57,22 @@ definition is resolved by `struct_layout.struct_def_name` from
57
57
  `struct_layout.type_dict.struct_dict`; enum definitions are resolved from the
58
58
  same `TypeDict`.
59
59
 
60
+ `encode` and `decode` accept an optional `progress_callback` argument typed as
61
+ `ProgressCallback`. The callback receives a `float` progress value calculated
62
+ from the top-level field extent: `(offset + size) / total_size`. Recursive
63
+ nested structure calls pass `None`, so callers receive progress updates only
64
+ for the top-level encode or decode operation.
65
+
66
+ ```python
67
+ from sltcodec import ProgressCallback, decode
68
+
69
+ def update_progress(progress: float) -> None:
70
+ progress_bar.value = progress
71
+
72
+ progress_callback: ProgressCallback = update_progress
73
+ decoded = decode(layout, data, progress_callback=progress_callback)
74
+ ```
75
+
60
76
  Layout persistence uses the explicit names `save_struct_layout` and
61
77
  `load_struct_layout`. The former `save_type_dict` and `load_type_dict` API is
62
78
  not part of the current public interface.
@@ -46,6 +46,26 @@ encoded = encode(layout, instance, bytearray())
46
46
  decoded = decode(layout, encoded)
47
47
  ```
48
48
 
49
+ ## Progress Callbacks
50
+
51
+ `encode` and `decode` accept an optional `progress_callback` argument. The
52
+ callback is typed as `ProgressCallback` and receives a `float` progress value
53
+ from `0.0` to `1.0`.
54
+
55
+ Progress is calculated for top-level fields as `(offset + size) / total_size`.
56
+ Nested structure encode/decode calls pass `None` internally, so recursive
57
+ fields do not emit additional callback events.
58
+
59
+ ```python
60
+ from sltcodec import ProgressCallback, decode
61
+
62
+ def update_progress(progress: float) -> None:
63
+ progress_bar.value = progress
64
+
65
+ progress_callback: ProgressCallback = update_progress
66
+ decoded = decode(layout, encoded, progress_callback=progress_callback)
67
+ ```
68
+
49
69
  `decode` returns a `StructInstance` whose `size` is the actual end of the
50
70
  decoded layout. `decode_field` stores the actual decoded size in the returned
51
71
  `FieldInstance.field_def.size`. For nested `StructDef` fields, that size comes
@@ -121,6 +141,7 @@ The following symbols are exported by `sltcodec.__all__` in this order:
121
141
  | Symbol | Description |
122
142
  | --- | --- |
123
143
  | `PRIMITIVE_TYPES` | Set of built-in field type names supported by the codec. |
144
+ | `ProgressCallback` | Callable type alias for encode/decode progress updates. |
124
145
  | `EnumDef` | Immutable definition of an enumeration. |
125
146
  | `EnumDict` | Dictionary-like container for `EnumDef` objects. |
126
147
  | `FieldDef` | Immutable definition of one structured field. |
@@ -293,15 +314,16 @@ beginning with `_` are internal helpers and are not part of the public API.
293
314
  | Definition | Signature | Description |
294
315
  | --- | --- | --- |
295
316
  | `PRIMITIVE_TYPES` | `set[str]` | Built-in field type names. |
317
+ | `ProgressCallback` | `Callable[[float], None]` | Progress callback type for top-level encode/decode updates. |
296
318
  | `save_struct_layout` | `(struct_layout, path) -> None` | Save a layout to a JSON file. |
297
319
  | `load_struct_layout` | `(path) -> StructLayout` | Load a layout from a JSON file. |
298
320
  | `save_struct_def_dict` | `(path, struct_def_dict) -> None` | Save a structure-definition dictionary through the layout format. |
299
321
  | `load_struct_def_dict` | `(path) -> dict[str, StructDef]` | Load a structure-definition dictionary. |
300
322
  | `save_enum_def_dict` | `(path, enum_def_dict) -> None` | Save an enum-definition dictionary through the layout format. |
301
323
  | `load_enum_def_dict` | `(path) -> dict[str, EnumDef]` | Load an enum-definition dictionary. |
302
- | `encode` | `(struct_layout, struct_instance, buf, padding_alignment_bits=32) -> bytearray` | Encode a complete structure. |
324
+ | `encode` | `(struct_layout, struct_instance, buf, padding_alignment_bits=32, progress_callback=None) -> bytearray` | Encode a complete structure. |
303
325
  | `decode_field` | `(field_def, data, env=None, type_dict=None, padding_alignment_bits=32) -> FieldInstance \| None` | Decode one field. |
304
- | `decode` | `(struct_layout, data, padding_alignment_bits=32) -> StructInstance` | Decode a complete structure. |
326
+ | `decode` | `(struct_layout, data, padding_alignment_bits=32, progress_callback=None) -> StructInstance` | Decode a complete structure. |
305
327
 
306
328
  `save_struct_def_dict`, `load_struct_def_dict`, `save_enum_def_dict`, and
307
329
  `load_enum_def_dict` are available from `sltcodec.codec` for dictionary-level
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sltcodec"
3
- version = "2.4.0"
3
+ version = "2.5.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"
@@ -1,10 +1,11 @@
1
- from .codec import (PRIMITIVE_TYPES, decode, encode, load_struct_layout,
2
- save_struct_layout)
1
+ from .codec import (PRIMITIVE_TYPES, ProgressCallback, decode, encode,
2
+ load_struct_layout, save_struct_layout)
3
3
  from .types import (EnumDef, EnumDict, FieldDef, FieldInstance, StructDef,
4
4
  StructDict, StructInstance, StructLayout, TypeDict)
5
5
 
6
6
  __all__ = [
7
7
  "PRIMITIVE_TYPES",
8
+ "ProgressCallback",
8
9
  "EnumDef",
9
10
  "EnumDict",
10
11
  "FieldDef",
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
  import json
5
5
  import re
6
6
  from pathlib import Path
7
- from typing import Any
7
+ from typing import Any, Callable
8
8
 
9
9
  from sltcalc import SltEval
10
10
  from sltcore import Info, InfoSize, bits_get, bits_set
@@ -27,6 +27,58 @@ _PRIMITIVE_TYPES = PRIMITIVE_TYPES
27
27
 
28
28
  _DEFAULT_PADDING_ALIGNMENT_BITS = 32
29
29
 
30
+ ProgressCallback = Callable[[float], None]
31
+
32
+
33
+ def _notify_progress(progress_callback: ProgressCallback | None,
34
+ offset: InfoSize, size: InfoSize, total_bits: int) -> None:
35
+ """Notify caller of top-level encode/decode progress."""
36
+ if progress_callback is None or total_bits <= 0:
37
+ return
38
+ progress = min((offset + size).bits / total_bits, 1.0)
39
+ progress_callback(progress)
40
+
41
+
42
+ def _estimate_encode_total_bits(struct_instance: StructInstance,
43
+ initial_size_bits: int) -> int:
44
+ """Estimate encoded top-level extent before mutating the output buffer."""
45
+ env: dict[str, Any] = {}
46
+ total_bits = max(struct_instance.size.bits, initial_size_bits)
47
+
48
+ for field_value in struct_instance.field_instances:
49
+ field_def = field_value.field_def
50
+ repeat_until_end = field_def.repeat == "end"
51
+ repeat = _resolve_repeat(field_def.repeat, env)
52
+ value = field_value.value
53
+ if _is_padding_field_def(field_def):
54
+ padding_size = _resolve_info_size(field_def.size, env)
55
+ value = bytearray(padding_size.bytes)
56
+ if repeat_until_end:
57
+ repeat_values = list(value)
58
+ repeat = len(repeat_values)
59
+ if repeat is not None and repeat > 1:
60
+ current_offset = _resolve_info_size(field_def.offset, env)
61
+ values = repeat_values if repeat_until_end else list(value)
62
+
63
+ for i in range(repeat):
64
+ field_def_repeat = _repeated_field_def(field_def, i,
65
+ current_offset,
66
+ field_def.size)
67
+ resolved_size = _resolve_info_size(field_def_repeat.size, env)
68
+ total_bits = max(total_bits,
69
+ (current_offset + resolved_size).bits)
70
+ env[field_def_repeat.name] = values[i]
71
+ if isinstance(current_offset, InfoSize):
72
+ current_offset += resolved_size
73
+ continue
74
+
75
+ resolved_offset = _resolve_info_size(field_def.offset, env)
76
+ resolved_size = _resolve_info_size(field_def.size, env)
77
+ total_bits = max(total_bits, (resolved_offset + resolved_size).bits)
78
+ env[field_def.name] = value
79
+
80
+ return total_bits
81
+
30
82
 
31
83
  def _as_field_defs(struct_def: StructDef | list[FieldDef]) -> list[FieldDef]:
32
84
  """Normalize StructDef/list inputs to a list of field definitions."""
@@ -134,8 +186,20 @@ def load_enum_def_dict(path: str | Path) -> dict[str, EnumDef]:
134
186
  def _resolve_info_size(value: InfoSize | str, env: dict[str, Any]) -> InfoSize:
135
187
  """Resolve an InfoSize value that can be static or expression-based."""
136
188
  if isinstance(value, str):
137
- stleval = SltEval(env)
138
- resolved_byte = stleval.eval(value)
189
+ eval_env = dict(env)
190
+
191
+ def replace_info_size(match: re.Match[str]) -> str:
192
+ name = f"_info_size_{len(eval_env)}"
193
+ eval_env[name] = InfoSize(int(match.group(1)), int(match.group(2)))
194
+ return name
195
+
196
+ expression = re.sub(
197
+ r"\bInfoSize\s*\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\)",
198
+ replace_info_size,
199
+ value,
200
+ )
201
+ stleval = SltEval(eval_env)
202
+ resolved_byte = stleval.eval(expression)
139
203
  if isinstance(resolved_byte, InfoSize):
140
204
  return resolved_byte
141
205
  return InfoSize(resolved_byte, 0)
@@ -164,6 +228,13 @@ def _is_padding_field_def(field_def: FieldDef) -> bool:
164
228
  and field_def.type in ["bytes", "bytearray"])
165
229
 
166
230
 
231
+ def _struct_def_has_dynamic_extent(struct_def: StructDef) -> bool:
232
+ """Check whether a nested structure can consume data beyond its fields."""
233
+ return any(field_def.repeat == "end" or isinstance(field_def.offset, str)
234
+ or isinstance(field_def.size, str)
235
+ for field_def in struct_def.fields)
236
+
237
+
167
238
  def _validate_padding_alignment_bits(padding_alignment_bits: int) -> None:
168
239
  """Validate padding alignment as a positive power-of-two bit size."""
169
240
  if (isinstance(padding_alignment_bits, bool)
@@ -234,12 +305,11 @@ def _prepare_field_info(
234
305
  raise TypeError(
235
306
  "Nested StructDef fields must be encoded with StructInstance "
236
307
  "values")
237
- nested_bytes = encode(_layout_for_struct_def(value.struct_def,
238
- type_dict,
239
- name=field_def.name),
240
- value,
241
- bytearray(),
242
- padding_alignment_bits=padding_alignment_bits)
308
+ nested_bytes = encode(
309
+ _layout_for_struct_def(value.struct_def,
310
+ type_dict,
311
+ name=field_def.name), value, bytearray(),
312
+ padding_alignment_bits, None)
243
313
  info = Info.from_bytes(bytes(nested_bytes), size, scale=field_def.scale)
244
314
  else:
245
315
  info = _encode_primitive(resolved_type, value, size, field_def.scale)
@@ -304,6 +374,7 @@ def encode(
304
374
  struct_instance: StructInstance,
305
375
  buf: bytearray,
306
376
  padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
377
+ progress_callback: ProgressCallback | None = None,
307
378
  ) -> bytearray:
308
379
  """Encode decode() result into a bytearray.
309
380
 
@@ -318,6 +389,9 @@ def encode(
318
389
  The base bytearray instance to write into.
319
390
  padding_alignment_bits : int, optional
320
391
  The padding alignment boundary in bits, by default 32.
392
+ progress_callback : Callable[[float], None] | None, optional
393
+ Called after each top-level field is encoded with progress in the
394
+ range 0.0 to 1.0. Nested recursive encodes are skipped.
321
395
 
322
396
  Returns
323
397
  -------
@@ -334,6 +408,7 @@ def encode(
334
408
  type_dict = struct_layout.type_dict
335
409
  env: dict[str, Any] = {}
336
410
  has_padding = False
411
+ total_bits = _estimate_encode_total_bits(struct_instance, len(buf) * 8)
337
412
 
338
413
  for field_value in struct_instance.field_instances:
339
414
  field_def = field_value.field_def
@@ -359,13 +434,19 @@ def encode(
359
434
  resolved_size = _resolve_info_size(field_def_repeat.size, env)
360
435
  encode_field(field_def_repeat, values[i], buf, env, type_dict,
361
436
  padding_alignment_bits)
437
+ _notify_progress(progress_callback, current_offset,
438
+ resolved_size, total_bits)
362
439
  env[field_def_repeat.name] = values[i]
363
440
  if isinstance(current_offset, InfoSize):
364
441
  current_offset += resolved_size
365
442
  continue
366
443
 
444
+ resolved_offset = _resolve_info_size(field_def.offset, env)
445
+ resolved_size = _resolve_info_size(field_def.size, env)
367
446
  encode_field(field_def, value, buf, env, type_dict,
368
447
  padding_alignment_bits)
448
+ _notify_progress(progress_callback, resolved_offset, resolved_size,
449
+ total_bits)
369
450
  env[field_def.name] = value
370
451
 
371
452
  if has_padding and struct_instance.size.bytes > len(buf):
@@ -415,8 +496,15 @@ def decode_field(
415
496
  nested_layout = _layout_for_struct_def(resolved_type,
416
497
  type_dict,
417
498
  name=field_def.name)
418
- nested_value = decode(nested_layout, bytearray(info.to_bytes),
419
- padding_alignment_bits)
499
+ nested_data = bytearray(info.to_bytes)
500
+ if _struct_def_has_dynamic_extent(resolved_type) and offset.bit == 0:
501
+ nested_data = bytearray(data[offset.byte:])
502
+ nested_value = decode(
503
+ nested_layout,
504
+ nested_data,
505
+ padding_alignment_bits,
506
+ None,
507
+ )
420
508
  actual_size = nested_value.size
421
509
  else:
422
510
  actual_size = info.info_size
@@ -485,6 +573,7 @@ def decode(
485
573
  struct_layout: StructLayout,
486
574
  data: bytearray | bytes,
487
575
  padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
576
+ progress_callback: ProgressCallback | None = None,
488
577
  ) -> StructInstance:
489
578
  """Decode a bytearray into field values according to a layout.
490
579
 
@@ -497,6 +586,9 @@ def decode(
497
586
  The data to decode.
498
587
  padding_alignment_bits : int, optional
499
588
  The padding alignment boundary in bits, by default 32.
589
+ progress_callback : Callable[[float], None] | None, optional
590
+ Called after each top-level field is decoded with progress in the
591
+ range 0.0 to 1.0. Nested recursive decodes are skipped.
500
592
  Returns
501
593
  -------
502
594
  StructInstance
@@ -582,6 +674,8 @@ def decode(
582
674
  if field_instance is not None:
583
675
  env[field_instance.field_def.name] = field_instance.value
584
676
  result.append_field_instance(field_instance)
677
+ _notify_progress(progress_callback, resolved_offset,
678
+ field_instance.field_def.size, data_bits)
585
679
  current_position = (resolved_offset +
586
680
  _resolve_info_size(field_def.size, env))
587
681
  continue
@@ -621,6 +715,8 @@ def decode(
621
715
  env[field_instance.field_def.name] = field_instance.value
622
716
  result.append_field_instance(field_instance)
623
717
  actual_size = field_instance.field_def.size
718
+ _notify_progress(progress_callback, current_offset,
719
+ actual_size, data_bits)
624
720
  else:
625
721
  actual_size = resolved_size
626
722
 
@@ -666,6 +762,8 @@ def decode(
666
762
  env[field_instance.field_def.name] = field_instance.value
667
763
  result.append_field_instance(field_instance)
668
764
  actual_size = field_instance.field_def.size
765
+ _notify_progress(progress_callback, current_offset, actual_size,
766
+ data_bits)
669
767
  else:
670
768
  actual_size = _resolve_info_size(field_def_repeat.size, env)
671
769
  if isinstance(current_offset, InfoSize) and isinstance(
@@ -5,8 +5,8 @@ import pytest
5
5
  from sltcore import InfoSize
6
6
 
7
7
  from sltcodec import (PRIMITIVE_TYPES, EnumDef, FieldDef, FieldInstance,
8
- StructDef, StructInstance, StructLayout, TypeDict, decode,
9
- encode)
8
+ ProgressCallback, StructDef, StructInstance, StructLayout,
9
+ TypeDict, decode, encode)
10
10
  from sltcodec.codec import _resolve_info_size, decode_field
11
11
 
12
12
 
@@ -42,6 +42,13 @@ def test_primitive_types_are_exposed_at_package_root():
42
42
  }
43
43
 
44
44
 
45
+ def test_progress_callback_type_is_exposed_at_package_root():
46
+ """Test that ProgressCallback is exposed for external consumers."""
47
+ progress_callback: ProgressCallback = lambda progress: None
48
+
49
+ assert callable(progress_callback)
50
+
51
+
45
52
  def test_encode_and_decode_round_trip():
46
53
  """Test that encoding and then decoding returns the original data."""
47
54
  struct_def = [
@@ -75,6 +82,44 @@ def test_encode_and_decode_round_trip():
75
82
  bytearray()) == encoded
76
83
 
77
84
 
85
+ def test_encode_and_decode_progress_callbacks_use_top_level_progress():
86
+ """Test progress callbacks report top-level encoded/decoded progress."""
87
+ fields = [
88
+ FieldDef(name="head",
89
+ offset=InfoSize(0, 0),
90
+ size=InfoSize(1, 0),
91
+ type="unsigned int"),
92
+ FieldDef(name="tail",
93
+ offset=InfoSize(1, 0),
94
+ size=InfoSize(1, 0),
95
+ type="unsigned int"),
96
+ ]
97
+ struct_def = StructDef(fields=fields)
98
+ struct_instance = StructInstance(
99
+ struct_def=struct_def,
100
+ field_instances=[
101
+ FieldInstance(fields[0], 0x12),
102
+ FieldInstance(fields[1], 0x34),
103
+ ],
104
+ )
105
+ encode_progress = []
106
+ decode_progress = []
107
+
108
+ encoded = encode(layout_for(struct_def),
109
+ struct_instance,
110
+ bytearray(),
111
+ progress_callback=encode_progress.append)
112
+ decoded = decode(layout_for(struct_def),
113
+ encoded,
114
+ progress_callback=decode_progress.append)
115
+
116
+ assert encoded == bytearray(b"\x12\x34")
117
+ assert [field_instance.value
118
+ for field_instance in decoded.field_instances] == [0x12, 0x34]
119
+ assert encode_progress == [0.5, 1.0]
120
+ assert decode_progress == [0.5, 1.0]
121
+
122
+
78
123
  def test_decode_accepts_struct_layout():
79
124
  """Decode a legacy field list via a StructLayout bundle."""
80
125
  struct_def = StructDef(
@@ -166,6 +211,33 @@ def test_encode_layout_handles_repeat():
166
211
  ]
167
212
 
168
213
 
214
+ def test_progress_callbacks_use_repeated_top_level_field_extent():
215
+ """Test repeated fields report progress against the encoded extent."""
216
+ field_def = FieldDef(name="value",
217
+ offset=InfoSize(0, 0),
218
+ size=InfoSize(1, 0),
219
+ type="unsigned int",
220
+ repeat=2)
221
+ struct_def = StructDef(fields=[field_def])
222
+ struct_instance = StructInstance(
223
+ struct_def=struct_def,
224
+ field_instances=[FieldInstance(field_def, [1, 2])],
225
+ )
226
+ encode_progress = []
227
+ decode_progress = []
228
+
229
+ encoded = encode(layout_for(struct_def),
230
+ struct_instance,
231
+ bytearray(),
232
+ progress_callback=encode_progress.append)
233
+ decode(layout_for([field_def]),
234
+ encoded,
235
+ progress_callback=decode_progress.append)
236
+
237
+ assert encode_progress == [0.5, 1.0]
238
+ assert decode_progress == [0.5, 1.0]
239
+
240
+
169
241
  def test_expression_repeat_uses_previous_field_value():
170
242
  """Test that repeat expressions are evaluated against the field env."""
171
243
  seed_field = FieldDef(name="count",
@@ -232,6 +304,38 @@ def test_repeat_end_stops_when_offset_is_out_of_range():
232
304
  assert [instance.value for instance in decoded.field_instances] == [None]
233
305
 
234
306
 
307
+ def test_info_size_expression_resolves_constructor():
308
+ """Test that serialized InfoSize constructors work in expressions."""
309
+ resolved = _resolve_info_size("InfoSize(1, 0) * count", {"count": 3})
310
+
311
+ assert resolved == InfoSize(3, 0)
312
+
313
+
314
+ def test_repeat_end_uses_dynamic_nested_struct_size():
315
+ """Test repeat='end' advances by a variable-size nested structure."""
316
+ length_field = FieldDef(name="length",
317
+ offset=InfoSize(0, 0),
318
+ size=InfoSize(1, 0),
319
+ type="unsigned int")
320
+ payload_field = FieldDef(name="payload",
321
+ offset=InfoSize(1, 0),
322
+ size="InfoSize(1, 0) * length",
323
+ type="bytearray")
324
+ record_field = FieldDef(
325
+ name="record",
326
+ offset=InfoSize(0, 0),
327
+ size=InfoSize(1, 0),
328
+ type=StructDef(fields=[length_field, payload_field]),
329
+ repeat="end")
330
+
331
+ decoded = decode(layout_for([record_field]), bytearray(b"\x02ab\x01c"))
332
+
333
+ assert [instance.field_def.name for instance in decoded.field_instances
334
+ ] == ["record[0]", "record[1]"]
335
+ assert [instance.value.size for instance in decoded.field_instances
336
+ ] == [InfoSize(3, 0), InfoSize(2, 0)]
337
+
338
+
235
339
  def test_decode_out_of_range_sets_current_and_tail_values_to_none():
236
340
  """Test out-of-range decode fills current/tail field values with None."""
237
341
  fields = [
@@ -331,6 +435,49 @@ def test_encode_recurses_for_nested_field_types():
331
435
  ]
332
436
 
333
437
 
438
+ def test_progress_callbacks_skip_nested_recursive_fields():
439
+ """Test nested recursive encode/decode calls do not report progress."""
440
+ child_field_defs = [
441
+ FieldDef(name="left",
442
+ offset=InfoSize(0, 0),
443
+ size=InfoSize(1, 0),
444
+ type="unsigned int"),
445
+ FieldDef(name="right",
446
+ offset=InfoSize(1, 0),
447
+ size=InfoSize(1, 0),
448
+ type="unsigned int"),
449
+ ]
450
+ parent_field_def = FieldDef(name="pair",
451
+ offset=InfoSize(0, 0),
452
+ size=InfoSize(2, 0),
453
+ type=StructDef(fields=child_field_defs))
454
+ child_instance = StructInstance(
455
+ struct_def=StructDef(fields=child_field_defs),
456
+ field_instances=[
457
+ FieldInstance(child_field_defs[0], 3),
458
+ FieldInstance(child_field_defs[1], 4),
459
+ ],
460
+ )
461
+ parent_struct_def = StructDef(fields=[parent_field_def])
462
+ parent_instance = StructInstance(
463
+ struct_def=parent_struct_def,
464
+ field_instances=[FieldInstance(parent_field_def, child_instance)],
465
+ )
466
+ encode_progress = []
467
+ decode_progress = []
468
+
469
+ encoded = encode(layout_for(parent_struct_def),
470
+ parent_instance,
471
+ bytearray(),
472
+ progress_callback=encode_progress.append)
473
+ decode(layout_for(parent_struct_def),
474
+ encoded,
475
+ progress_callback=decode_progress.append)
476
+
477
+ assert encode_progress == [1.0]
478
+ assert decode_progress == [1.0]
479
+
480
+
334
481
  def test_decode_field_uses_nested_struct_instance_size():
335
482
  """Test that nested decoding determines the field's actual size."""
336
483
  child_field_def = FieldDef(name="value",
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