sltcodec 2.4.1__tar.gz → 2.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sltcodec
3
- Version: 2.4.1
3
+ Version: 2.5.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
@@ -12,7 +12,7 @@ License-File: LICENSE
12
12
  Keywords: byte,deserialization,layout,serialization,struct
13
13
  Requires-Python: >=3.12
14
14
  Requires-Dist: sltcalc>=1.0.0
15
- Requires-Dist: sltcore>=1.5.0
15
+ Requires-Dist: sltcore>=1.6.0
16
16
  Description-Content-Type: text/markdown
17
17
 
18
18
  # StructLayoutToolkitCodec
@@ -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,12 +1,12 @@
1
1
  [project]
2
2
  name = "sltcodec"
3
- version = "2.4.1"
3
+ version = "2.5.1"
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
8
  "sltcalc>=1.0.0",
9
- "sltcore>=1.5.0",
9
+ "sltcore>=1.6.0",
10
10
  ]
11
11
  license = "MIT"
12
12
  license-files = ["LICENSE"]
@@ -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."""
@@ -253,12 +305,11 @@ def _prepare_field_info(
253
305
  raise TypeError(
254
306
  "Nested StructDef fields must be encoded with StructInstance "
255
307
  "values")
256
- nested_bytes = encode(_layout_for_struct_def(value.struct_def,
257
- type_dict,
258
- name=field_def.name),
259
- value,
260
- bytearray(),
261
- 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)
262
313
  info = Info.from_bytes(bytes(nested_bytes), size, scale=field_def.scale)
263
314
  else:
264
315
  info = _encode_primitive(resolved_type, value, size, field_def.scale)
@@ -323,6 +374,7 @@ def encode(
323
374
  struct_instance: StructInstance,
324
375
  buf: bytearray,
325
376
  padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
377
+ progress_callback: ProgressCallback | None = None,
326
378
  ) -> bytearray:
327
379
  """Encode decode() result into a bytearray.
328
380
 
@@ -337,6 +389,9 @@ def encode(
337
389
  The base bytearray instance to write into.
338
390
  padding_alignment_bits : int, optional
339
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.
340
395
 
341
396
  Returns
342
397
  -------
@@ -353,6 +408,7 @@ def encode(
353
408
  type_dict = struct_layout.type_dict
354
409
  env: dict[str, Any] = {}
355
410
  has_padding = False
411
+ total_bits = _estimate_encode_total_bits(struct_instance, len(buf) * 8)
356
412
 
357
413
  for field_value in struct_instance.field_instances:
358
414
  field_def = field_value.field_def
@@ -378,13 +434,19 @@ def encode(
378
434
  resolved_size = _resolve_info_size(field_def_repeat.size, env)
379
435
  encode_field(field_def_repeat, values[i], buf, env, type_dict,
380
436
  padding_alignment_bits)
437
+ _notify_progress(progress_callback, current_offset,
438
+ resolved_size, total_bits)
381
439
  env[field_def_repeat.name] = values[i]
382
440
  if isinstance(current_offset, InfoSize):
383
441
  current_offset += resolved_size
384
442
  continue
385
443
 
444
+ resolved_offset = _resolve_info_size(field_def.offset, env)
445
+ resolved_size = _resolve_info_size(field_def.size, env)
386
446
  encode_field(field_def, value, buf, env, type_dict,
387
447
  padding_alignment_bits)
448
+ _notify_progress(progress_callback, resolved_offset, resolved_size,
449
+ total_bits)
388
450
  env[field_def.name] = value
389
451
 
390
452
  if has_padding and struct_instance.size.bytes > len(buf):
@@ -437,8 +499,12 @@ def decode_field(
437
499
  nested_data = bytearray(info.to_bytes)
438
500
  if _struct_def_has_dynamic_extent(resolved_type) and offset.bit == 0:
439
501
  nested_data = bytearray(data[offset.byte:])
440
- nested_value = decode(nested_layout, nested_data,
441
- padding_alignment_bits)
502
+ nested_value = decode(
503
+ nested_layout,
504
+ nested_data,
505
+ padding_alignment_bits,
506
+ None,
507
+ )
442
508
  actual_size = nested_value.size
443
509
  else:
444
510
  actual_size = info.info_size
@@ -507,6 +573,7 @@ def decode(
507
573
  struct_layout: StructLayout,
508
574
  data: bytearray | bytes,
509
575
  padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
576
+ progress_callback: ProgressCallback | None = None,
510
577
  ) -> StructInstance:
511
578
  """Decode a bytearray into field values according to a layout.
512
579
 
@@ -519,6 +586,9 @@ def decode(
519
586
  The data to decode.
520
587
  padding_alignment_bits : int, optional
521
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.
522
592
  Returns
523
593
  -------
524
594
  StructInstance
@@ -604,6 +674,8 @@ def decode(
604
674
  if field_instance is not None:
605
675
  env[field_instance.field_def.name] = field_instance.value
606
676
  result.append_field_instance(field_instance)
677
+ _notify_progress(progress_callback, resolved_offset,
678
+ field_instance.field_def.size, data_bits)
607
679
  current_position = (resolved_offset +
608
680
  _resolve_info_size(field_def.size, env))
609
681
  continue
@@ -643,6 +715,8 @@ def decode(
643
715
  env[field_instance.field_def.name] = field_instance.value
644
716
  result.append_field_instance(field_instance)
645
717
  actual_size = field_instance.field_def.size
718
+ _notify_progress(progress_callback, current_offset,
719
+ actual_size, data_bits)
646
720
  else:
647
721
  actual_size = resolved_size
648
722
 
@@ -688,6 +762,8 @@ def decode(
688
762
  env[field_instance.field_def.name] = field_instance.value
689
763
  result.append_field_instance(field_instance)
690
764
  actual_size = field_instance.field_def.size
765
+ _notify_progress(progress_callback, current_offset, actual_size,
766
+ data_bits)
691
767
  else:
692
768
  actual_size = _resolve_info_size(field_def_repeat.size, env)
693
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",
@@ -363,6 +435,49 @@ def test_encode_recurses_for_nested_field_types():
363
435
  ]
364
436
 
365
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
+
366
481
  def test_decode_field_uses_nested_struct_instance_size():
367
482
  """Test that nested decoding determines the field's actual size."""
368
483
  child_field_def = FieldDef(name="value",
@@ -74,7 +74,7 @@ wheels = [
74
74
 
75
75
  [[package]]
76
76
  name = "sltcodec"
77
- version = "2.3.0"
77
+ version = "2.5.0"
78
78
  source = { editable = "." }
79
79
  dependencies = [
80
80
  { name = "sltcalc" },
@@ -89,7 +89,7 @@ dev = [
89
89
  [package.metadata]
90
90
  requires-dist = [
91
91
  { name = "sltcalc", specifier = ">=1.0.0" },
92
- { name = "sltcore", specifier = ">=1.5.0" },
92
+ { name = "sltcore", specifier = ">=1.6.0" },
93
93
  ]
94
94
 
95
95
  [package.metadata.requires-dev]
@@ -97,9 +97,9 @@ dev = [{ name = "pytest", specifier = ">=9.1.1" }]
97
97
 
98
98
  [[package]]
99
99
  name = "sltcore"
100
- version = "1.5.0"
100
+ version = "1.6.0"
101
101
  source = { registry = "https://pypi.org/simple" }
102
- sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/692c45c8d007b71b627fb4c53d90ecb5ea1168d892dc14e5ad3ecae0b8d3/sltcore-1.5.0.tar.gz", hash = "sha256:26280f743947f22e31e334fd793679f3aad19367bbf9bfddb6215123d7bd2d18", size = 18050, upload-time = "2026-08-11T13:14:51.241Z" }
102
+ sdist = { url = "https://files.pythonhosted.org/packages/32/01/89dc1e6c0429710d7329a3dab769e1c9f8b148ab1592c69df53592dbf8c2/sltcore-1.6.0.tar.gz", hash = "sha256:872ada7183ae5f065a40dc44ee5062bb7cc781a4758b56cd740bb77e9e1fdc6e", size = 20648, upload-time = "2026-08-21T14:49:00.72Z" }
103
103
  wheels = [
104
- { url = "https://files.pythonhosted.org/packages/0f/ca/38d1ab93a871ba9e60e500b93f8c48fdb2be3308a8ab08e6ea0a73e516c2/sltcore-1.5.0-py3-none-any.whl", hash = "sha256:148611c58329fcadc793b38a973412144c073a036516baf9430b3170e71180c4", size = 10458, upload-time = "2026-08-11T13:14:50.295Z" },
104
+ { url = "https://files.pythonhosted.org/packages/a4/b5/d35ac0fcab3a16efdc80814a59d1788a962e8ac8d8005dc6c18435baf655/sltcore-1.6.0-py3-none-any.whl", hash = "sha256:d3bf525275fda9159b97c2663d14bb73b56b70869e07f0d79727883bcbedd8d8", size = 12707, upload-time = "2026-08-21T14:48:59.749Z" },
105
105
  ]
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