sltcodec 2.1.0__tar.gz → 2.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: 2.1.0
3
+ Version: 2.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
@@ -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
 
@@ -63,6 +63,12 @@ encoded = encode(layout, instance, bytearray())
63
63
  decoded = decode(layout, encoded)
64
64
  ```
65
65
 
66
+ `decode` returns a `StructInstance` whose `size` is the actual end of the
67
+ decoded layout. `decode_field` stores the actual decoded size in the returned
68
+ `FieldInstance.field_def.size`. For nested `StructDef` fields, that size comes
69
+ from the nested `StructInstance.size`; for repeated fields, the next element's
70
+ offset advances by the actual size of the previous element.
71
+
66
72
  ## Field Types
67
73
 
68
74
  The public `PRIMITIVE_TYPES` set contains:
@@ -78,7 +84,6 @@ The public `PRIMITIVE_TYPES` set contains:
78
84
  Store named structures in `TypeDict.struct_dict` and enums in `TypeDict.enum_dict`. A field refers to an enum by its `enum_def_name`; the codec resolves it through `StructLayout.type_dict`.
79
85
 
80
86
  ```python
81
- | `__lt__(other)` | Compare enum definitions using their serialized ordering. |
82
87
  from sltcore import InfoSize
83
88
  from sltcodec import EnumDef, FieldDef, StructDef, StructLayout, TypeDict, decode
84
89
 
@@ -102,7 +107,6 @@ layout = StructLayout(
102
107
  )
103
108
 
104
109
  decoded = decode(layout, bytearray(b"\x01"))
105
- | `__lt__(other)` | Compare field definitions using their serialized ordering. |
106
110
  assert decoded.field_instances[0].enum_item == ("NG", 1)
107
111
  ```
108
112
 
@@ -117,7 +121,6 @@ from sltcodec import load_struct_layout, save_struct_layout
117
121
  path = Path("struct_layout.json")
118
122
  save_struct_layout(layout, path)
119
123
  loaded = load_struct_layout(path)
120
- | `__lt__(other)` | Compare field instances using field-definition order. |
121
124
  assert loaded == layout
122
125
  ```
123
126
 
@@ -185,7 +188,6 @@ internal implementation details and are omitted from the public method tables.
185
188
 
186
189
  | Method | Description |
187
190
  | --- | --- |
188
- | `split_repeat(index, offset=None, size=None)` | Create one field definition from a repeated field. |
189
191
  | `to_dict()` | Convert the definition to a JSON-compatible dictionary. |
190
192
  | `to_json()` | Convert the definition to a JSON string. |
191
193
  | `from_dict(data)` | Create a `FieldDef` from a dictionary. |
@@ -74,6 +74,12 @@ updated_field = field_instance.with_value(
74
74
  )
75
75
  ```
76
76
 
77
+ Decoded sizes reflect the actual data consumed. `decode` sets
78
+ `StructInstance.size` to the end of the decoded layout, and `decode_field`
79
+ stores the actual size in `FieldInstance.field_def.size`. For nested structures,
80
+ the nested `StructInstance.size` is used; repeated fields advance each next
81
+ offset by the preceding element's actual size.
82
+
77
83
  ## Versioning And Release
78
84
 
79
85
  Version bump scripts are provided for patch, minor, and major releases:
@@ -46,6 +46,12 @@ encoded = encode(layout, instance, bytearray())
46
46
  decoded = decode(layout, encoded)
47
47
  ```
48
48
 
49
+ `decode` returns a `StructInstance` whose `size` is the actual end of the
50
+ decoded layout. `decode_field` stores the actual decoded size in the returned
51
+ `FieldInstance.field_def.size`. For nested `StructDef` fields, that size comes
52
+ from the nested `StructInstance.size`; for repeated fields, the next element's
53
+ offset advances by the actual size of the previous element.
54
+
49
55
  ## Field Types
50
56
 
51
57
  The public `PRIMITIVE_TYPES` set contains:
@@ -61,7 +67,6 @@ The public `PRIMITIVE_TYPES` set contains:
61
67
  Store named structures in `TypeDict.struct_dict` and enums in `TypeDict.enum_dict`. A field refers to an enum by its `enum_def_name`; the codec resolves it through `StructLayout.type_dict`.
62
68
 
63
69
  ```python
64
- | `__lt__(other)` | Compare enum definitions using their serialized ordering. |
65
70
  from sltcore import InfoSize
66
71
  from sltcodec import EnumDef, FieldDef, StructDef, StructLayout, TypeDict, decode
67
72
 
@@ -85,7 +90,6 @@ layout = StructLayout(
85
90
  )
86
91
 
87
92
  decoded = decode(layout, bytearray(b"\x01"))
88
- | `__lt__(other)` | Compare field definitions using their serialized ordering. |
89
93
  assert decoded.field_instances[0].enum_item == ("NG", 1)
90
94
  ```
91
95
 
@@ -100,7 +104,6 @@ from sltcodec import load_struct_layout, save_struct_layout
100
104
  path = Path("struct_layout.json")
101
105
  save_struct_layout(layout, path)
102
106
  loaded = load_struct_layout(path)
103
- | `__lt__(other)` | Compare field instances using field-definition order. |
104
107
  assert loaded == layout
105
108
  ```
106
109
 
@@ -168,7 +171,6 @@ internal implementation details and are omitted from the public method tables.
168
171
 
169
172
  | Method | Description |
170
173
  | --- | --- |
171
- | `split_repeat(index, offset=None, size=None)` | Create one field definition from a repeated field. |
172
174
  | `to_dict()` | Convert the definition to a JSON-compatible dictionary. |
173
175
  | `to_json()` | Convert the definition to a JSON string. |
174
176
  | `from_dict(data)` | Create a `FieldDef` from a dictionary. |
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "sltcodec"
3
- version = "2.1.0"
3
+ version = "2.2.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"
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
+ import re
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
 
@@ -135,20 +136,12 @@ def _resolve_info_size(value: InfoSize | str, env: dict[str, Any]) -> InfoSize:
135
136
  if isinstance(value, str):
136
137
  stleval = SltEval(env)
137
138
  resolved_byte = stleval.eval(value)
139
+ if isinstance(resolved_byte, InfoSize):
140
+ return resolved_byte
138
141
  return InfoSize(resolved_byte, 0)
139
142
  return value
140
143
 
141
144
 
142
- def _resolve_offset(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
143
- """Resolve a field offset that can be static or expression-based."""
144
- return _resolve_info_size(field_def.offset, env)
145
-
146
-
147
- def _resolve_size(field_def: FieldDef, env: dict[str, Any]) -> InfoSize:
148
- """Resolve a field size that can be static or expression-based."""
149
- return _resolve_info_size(field_def.size, env)
150
-
151
-
152
145
  def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
153
146
  """Resolve a byte-swap flag that can be static or expression-based."""
154
147
  if isinstance(field_def.byte_swap, str):
@@ -156,27 +149,12 @@ def _resolve_byte_swap(field_def: FieldDef, env: dict[str, Any]) -> bool:
156
149
  return field_def.byte_swap
157
150
 
158
151
 
159
- def _byte_swap_info(info: Info, size: InfoSize) -> Info:
160
- """Reverse the byte representation of an Info payload."""
161
- return Info.from_bytes(info.to_bytes[::-1], size, scale=info.scale)
162
-
163
-
164
152
  def _is_padding_field_def(field_def: FieldDef) -> bool:
165
153
  """Check whether a field definition represents padding."""
166
154
  return (field_def.name.startswith("padding[")
167
155
  and field_def.type in ["bytes", "bytearray"])
168
156
 
169
157
 
170
- def _info_size_to_bits(info_size: InfoSize) -> int:
171
- """Convert InfoSize to an absolute bit count."""
172
- return info_size.byte * 8 + info_size.bit
173
-
174
-
175
- def _info_size_from_bits(total_bits: int) -> InfoSize:
176
- """Create InfoSize from an absolute bit count."""
177
- return InfoSize(total_bits >> 3, total_bits & 0x7)
178
-
179
-
180
158
  def _validate_padding_alignment_bits(padding_alignment_bits: int) -> None:
181
159
  """Validate padding alignment as a positive power-of-two bit size."""
182
160
  if (isinstance(padding_alignment_bits, bool)
@@ -236,8 +214,8 @@ def _prepare_field_info(
236
214
  padding_alignment_bits: int = _DEFAULT_PADDING_ALIGNMENT_BITS,
237
215
  ) -> tuple[InfoSize, InfoSize, Info] | None:
238
216
  """Resolve a field definition into an offset, size, and info payload."""
239
- offset = _resolve_offset(field_def, env)
240
- size = _resolve_size(field_def, env)
217
+ offset = _resolve_info_size(field_def.offset, env)
218
+ size = _resolve_info_size(field_def.size, env)
241
219
  if size.byte == 0 and size.bit == 0:
242
220
  return None
243
221
 
@@ -259,17 +237,30 @@ def _prepare_field_info(
259
237
  return offset, size, info
260
238
 
261
239
 
262
- def _split_repeated_field(field_def: FieldDef,
263
- index: int,
264
- env: dict[str, Any],
265
- offset: InfoSize | str | None = None) -> FieldDef:
266
- """Create a repeated-field definition with resolved offset and size."""
267
- resolved_offset = (_resolve_offset(field_def, env)
268
- if offset is None else offset)
269
- resolved_size = _resolve_size(field_def, env)
270
- return field_def.split_repeat(index,
271
- offset=resolved_offset,
272
- size=resolved_size)
240
+ def _repeated_field_def(field_def: FieldDef, index: int, offset: InfoSize,
241
+ size: InfoSize | str) -> FieldDef:
242
+ """Create a resolved field definition for one repeated value."""
243
+ repeated_name = f"{field_def.name}[{index}]"
244
+
245
+ def replace_name(value: Any) -> Any:
246
+ if not isinstance(value, str):
247
+ return value
248
+ pattern = (rf"(?<![0-9A-Za-z_]){re.escape(field_def.name)}"
249
+ rf"(?![0-9A-Za-z_])")
250
+ return re.sub(pattern, repeated_name, value)
251
+
252
+ return FieldDef(
253
+ name=repeated_name,
254
+ offset=offset,
255
+ size=size,
256
+ type=replace_name(field_def.type),
257
+ scale=field_def.scale,
258
+ repeat=None,
259
+ description=field_def.description,
260
+ range_expression=replace_name(field_def.range_expression),
261
+ enum_def_name=field_def.enum_def_name,
262
+ byte_swap=replace_name(field_def.byte_swap),
263
+ )
273
264
 
274
265
 
275
266
  def encode_field(
@@ -291,7 +282,7 @@ def encode_field(
291
282
 
292
283
  offset, size, info = prepared
293
284
  if _resolve_byte_swap(field_def, env):
294
- info = _byte_swap_info(info, size)
285
+ info = info.byte_swap
295
286
  required_bytes = (offset + size).bytes
296
287
  if len(buf) < required_bytes:
297
288
  buf.extend(b"\x00" * (required_bytes - len(buf)))
@@ -341,21 +332,21 @@ def encode(
341
332
  has_padding = True
342
333
  value = field_value.value
343
334
  if _is_padding_field_def(field_def):
344
- padding_size = _resolve_size(field_def, env)
335
+ padding_size = _resolve_info_size(field_def.size, env)
345
336
  value = bytearray(padding_size.bytes)
346
337
  if field_def.repeat is not None and field_def.repeat > 1:
347
- current_offset = _resolve_offset(field_def, env)
348
- resolved_size = _resolve_size(field_def, env)
338
+ current_offset = _resolve_info_size(field_def.offset, env)
349
339
  values = list(value)
350
340
 
351
341
  for i in range(field_def.repeat):
352
- field_def_repeat = _split_repeated_field(
353
- field_def, i, env, current_offset)
342
+ field_def_repeat = _repeated_field_def(field_def, i,
343
+ current_offset,
344
+ field_def.size)
345
+ resolved_size = _resolve_info_size(field_def_repeat.size, env)
354
346
  encode_field(field_def_repeat, values[i], buf, env, type_dict,
355
347
  padding_alignment_bits)
356
348
  env[field_def_repeat.name] = values[i]
357
- if isinstance(current_offset, InfoSize) and isinstance(
358
- resolved_size, InfoSize):
349
+ if isinstance(current_offset, InfoSize):
359
350
  current_offset += resolved_size
360
351
  continue
361
352
 
@@ -396,19 +387,29 @@ def decode_field(
396
387
  """
397
388
  if env is None:
398
389
  env = {}
399
- offset = _resolve_offset(field_def, env)
400
- size = _resolve_size(field_def, env)
390
+ offset = _resolve_info_size(field_def.offset, env)
391
+ size = _resolve_info_size(field_def.size, env)
401
392
  if size.byte == 0 and size.bit == 0:
402
393
  return None
403
394
  info = bits_get(data, offset, size, scale=field_def.scale)
404
395
  byte_swap = _resolve_byte_swap(field_def, env)
405
396
  if byte_swap:
406
- info = _byte_swap_info(info, size)
397
+ info = info.byte_swap
407
398
  resolved_type = _resolve_field_type(field_def.type, type_dict, env)
399
+ nested_value = None
400
+ if isinstance(resolved_type, StructDef):
401
+ nested_layout = _layout_for_struct_def(resolved_type,
402
+ type_dict,
403
+ name=field_def.name)
404
+ nested_value = decode(nested_layout, bytearray(info.to_bytes),
405
+ padding_alignment_bits)
406
+ actual_size = nested_value.size
407
+ else:
408
+ actual_size = info.info_size
408
409
  resolved_field_def = FieldDef(
409
410
  name=field_def.name,
410
411
  offset=offset,
411
- size=size,
412
+ size=actual_size,
412
413
  type=resolved_type,
413
414
  scale=field_def.scale,
414
415
  repeat=field_def.repeat,
@@ -418,13 +419,9 @@ def decode_field(
418
419
  byte_swap=byte_swap,
419
420
  )
420
421
  if isinstance(resolved_type, StructDef):
421
- nested_layout = _layout_for_struct_def(resolved_type,
422
- type_dict,
423
- name=field_def.name)
424
422
  return FieldInstance(
425
423
  field_def=resolved_field_def,
426
- value=decode(nested_layout, bytearray(info.to_bytes),
427
- padding_alignment_bits),
424
+ value=nested_value,
428
425
  )
429
426
  if resolved_type == "bool":
430
427
  return FieldInstance.from_value(resolved_field_def,
@@ -506,22 +503,21 @@ def decode(
506
503
 
507
504
  def append_padding_until(target_offset: InfoSize) -> None:
508
505
  nonlocal current_position, padding_index
509
- start_bits = _info_size_to_bits(current_position)
510
- end_bits = _info_size_to_bits(target_offset)
506
+ start_bits = current_position.bits
507
+ end_bits = target_offset.bits
511
508
  if end_bits <= start_bits:
512
509
  return
513
510
 
514
511
  chunk_start_bits = start_bits
515
512
  while chunk_start_bits < end_bits:
516
- chunk_start = _info_size_from_bits(chunk_start_bits)
517
- bits_to_boundary = _info_size_to_bits(
518
- chunk_start.align_to(padding_alignment_bits))
513
+ chunk_start = InfoSize(0, chunk_start_bits)
514
+ bits_to_boundary = chunk_start.align_to(padding_alignment_bits).bits
519
515
  chunk_size_bits = bits_to_boundary or padding_alignment_bits
520
516
  chunk_end_bits = min(end_bits, chunk_start_bits + chunk_size_bits)
521
517
  padding_field_def = FieldDef(
522
518
  name=f"padding[{padding_index}]",
523
- offset=_info_size_from_bits(chunk_start_bits),
524
- size=_info_size_from_bits(chunk_end_bits - chunk_start_bits),
519
+ offset=InfoSize(0, chunk_start_bits),
520
+ size=InfoSize(0, chunk_end_bits - chunk_start_bits),
525
521
  type="bytes",
526
522
  description="Auto-generated padding",
527
523
  )
@@ -536,7 +532,7 @@ def decode(
536
532
  for field_def in _as_field_defs(struct_def_obj):
537
533
  # Handle non-repeated fields
538
534
  if field_def.repeat is None or field_def.repeat <= 1:
539
- resolved_offset = _resolve_offset(field_def, env)
535
+ resolved_offset = _resolve_info_size(field_def.offset, env)
540
536
  append_padding_until(resolved_offset)
541
537
  field_instance = decode_field(field_def, data, env, type_dict,
542
538
  padding_alignment_bits)
@@ -544,23 +540,36 @@ def decode(
544
540
  env[field_instance.field_def.name] = field_instance.value
545
541
  result.append_field_instance(field_instance)
546
542
  current_position = (resolved_offset +
547
- _resolve_size(field_def, env))
543
+ _resolve_info_size(field_def.size, env))
548
544
  continue
549
545
  # Handle repeated fields
550
- current_offset = _resolve_offset(field_def, env)
551
- resolved_size = _resolve_size(field_def, env)
546
+ current_offset = _resolve_info_size(field_def.offset, env)
552
547
  append_padding_until(current_offset)
553
548
  for i in range(field_def.repeat):
554
- field_def_repeat = _split_repeated_field(field_def, i, env,
555
- current_offset)
549
+ field_def_repeat = _repeated_field_def(field_def, i, current_offset,
550
+ field_def.size)
556
551
  field_instance = decode_field(field_def_repeat, data, env,
557
552
  type_dict, padding_alignment_bits)
558
553
  if field_instance is not None:
559
554
  env[field_instance.field_def.name] = field_instance.value
560
555
  result.append_field_instance(field_instance)
561
- current_position = current_offset + resolved_size
562
- current_offset += resolved_size
563
-
556
+ actual_size = field_instance.field_def.size
557
+ else:
558
+ actual_size = _resolve_info_size(field_def_repeat.size, env)
559
+ if isinstance(current_offset, InfoSize) and isinstance(
560
+ actual_size, InfoSize):
561
+ current_offset += actual_size
562
+ current_position = current_offset
563
+
564
+ actual_size = InfoSize(0, 0)
565
+ for field_instance in result.field_instances:
566
+ field_def = field_instance.field_def
567
+ if (isinstance(field_def.offset, InfoSize)
568
+ and isinstance(field_def.size, InfoSize)):
569
+ field_end = field_def.offset + field_def.size
570
+ if field_end > actual_size:
571
+ actual_size = field_end
572
+ result.size = actual_size
564
573
  append_padding_until(result.size)
565
574
 
566
575
  return result
@@ -2,7 +2,6 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
- import re
6
5
  from collections.abc import MutableMapping
7
6
  from dataclasses import dataclass, field
8
7
  from functools import total_ordering
@@ -112,46 +111,6 @@ class FieldDef:
112
111
  default=False,
113
112
  metadata={"desc": "Whether to reverse the field byte order"})
114
113
 
115
- def split_repeat(self,
116
- index: int,
117
- offset: InfoSize | str | None = None,
118
- size: InfoSize | str | None = None) -> "FieldDef":
119
- """Create a single repeated-field definition for the given index."""
120
- split_name = f"{self.name}[{index}]"
121
- split_offset_source = self.offset if offset is None else offset
122
- split_size_source = self.size if size is None else size
123
- split_offset = self._replace_name_in_expression(split_offset_source,
124
- self.name, split_name)
125
- split_size = self._replace_name_in_expression(split_size_source,
126
- self.name, split_name)
127
- split_type = self._replace_name_in_expression(self.type, self.name,
128
- split_name)
129
- split_range_expression = self._replace_name_in_expression(
130
- self.range_expression, self.name, split_name)
131
- split_byte_swap = self._replace_name_in_expression(
132
- self.byte_swap, self.name, split_name)
133
-
134
- return FieldDef(name=split_name,
135
- offset=split_offset,
136
- size=split_size,
137
- type=split_type,
138
- scale=self.scale,
139
- repeat=None,
140
- description=self.description,
141
- range_expression=split_range_expression,
142
- enum_def_name=self.enum_def_name,
143
- byte_swap=split_byte_swap)
144
-
145
- @staticmethod
146
- def _replace_name_in_expression(value: Any, old_name: str,
147
- new_name: str) -> Any:
148
- """Replace standalone old_name tokens in expression-like strings."""
149
- if not isinstance(value, str):
150
- return value
151
- pattern = (rf"(?<![0-9A-Za-z_]){re.escape(old_name)}"
152
- rf"(?![0-9A-Za-z_])")
153
- return re.sub(pattern, lambda _: new_name, value)
154
-
155
114
  def __lt__(self, other: object) -> bool:
156
115
  """Compare field definitions using a stable serialized sort key."""
157
116
  if not isinstance(other, FieldDef):
@@ -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):
@@ -215,6 +215,24 @@ def test_encode_recurses_for_nested_field_types():
215
215
  ]
216
216
 
217
217
 
218
+ def test_decode_field_uses_nested_struct_instance_size():
219
+ """Test that nested decoding determines the field's actual size."""
220
+ child_field_def = FieldDef(name="value",
221
+ offset=InfoSize(0, 0),
222
+ size=InfoSize(1, 0),
223
+ type="unsigned int")
224
+ parent_field_def = FieldDef(name="nested",
225
+ offset=InfoSize(0, 0),
226
+ size=InfoSize(4, 0),
227
+ type=StructDef(fields=[child_field_def]))
228
+
229
+ decoded = decode_field(parent_field_def, bytearray(b"\x7f"))
230
+
231
+ assert decoded is not None
232
+ assert decoded.field_def.size == InfoSize(1, 0)
233
+ assert decoded.value.size == InfoSize(1, 0)
234
+
235
+
218
236
  def test_repeated_field_preserves_description():
219
237
  """Test that repeated fields keep their description on decoded entries."""
220
238
  field_def = FieldDef(name="value",
@@ -572,6 +590,46 @@ def test_decode_field_returns_field_instance():
572
590
  assert decoded == FieldInstance(field_def=field_def, value=127)
573
591
 
574
592
 
593
+ def test_decode_field_sets_actual_size_on_field_instance():
594
+ """Test that decode_field stores the resolved Info size."""
595
+ field_def = FieldDef(name="value",
596
+ offset="offset",
597
+ size="field_size",
598
+ type="unsigned int")
599
+
600
+ decoded = decode_field(field_def,
601
+ bytearray(b"\x7f"),
602
+ env={
603
+ "offset": 0,
604
+ "field_size": 1
605
+ })
606
+
607
+ assert decoded is not None
608
+ assert decoded.field_def.size == InfoSize(1, 0)
609
+
610
+
611
+ def test_resolve_info_size_returns_expression_info_size_unchanged():
612
+ """Test that expressions resolving to InfoSize preserve the result."""
613
+ info_size = InfoSize(2, 3)
614
+
615
+ resolved = _resolve_info_size("field_size", {"field_size": info_size})
616
+
617
+ assert resolved is info_size
618
+
619
+
620
+ def test_decode_struct_instance_size_matches_decoded_layout():
621
+ """Test that decode reports the complete decoded layout size."""
622
+ field_def = FieldDef(name="value",
623
+ offset=InfoSize(2, 0),
624
+ size=InfoSize(2, 0),
625
+ type="unsigned int")
626
+ data = bytearray(b"\x00\x00\x7f\x00")
627
+
628
+ decoded = decode(layout_for(StructDef(fields=[field_def])), data)
629
+
630
+ assert decoded.size == InfoSize(len(data), 0)
631
+
632
+
575
633
  def test_encode_padding_fields_use_zero_value_and_extend_to_struct_size():
576
634
  """Test that padding fields are zeroed and trailing bytes are padded.
577
635
 
@@ -13,23 +13,6 @@ class ValueKind(Enum):
13
13
  B = 2
14
14
 
15
15
 
16
- def test_split_repeat_replaces_name_in_range_expression():
17
- """Test split_repeat replaces field-name tokens in range expressions."""
18
- field_def = FieldDef(
19
- name="value",
20
- offset=InfoSize(0, 0),
21
- size=InfoSize(1, 0),
22
- type="unsigned int",
23
- repeat=2,
24
- range_expression=("0 <= value and value <= limit and value_count"))
25
-
26
- split = field_def.split_repeat(1)
27
-
28
- assert split.name == "value[1]"
29
- assert split.range_expression == (
30
- "0 <= value[1] and value[1] <= limit and value_count")
31
-
32
-
33
16
  def test_field_def_json_round_trip():
34
17
  """Test that FieldDef can be serialized and deserialized from JSON."""
35
18
  field_def = FieldDef(name="value",
@@ -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 = "1.3.0"
77
+ version = "2.1.1"
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