sltcodec 0.2.0__tar.gz → 0.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
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: sltcodec
3
- Version: 0.2.0
3
+ Version: 0.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sltcodec"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Decode and encode bytearrays according to struct layout definitions using sltcore."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -170,13 +170,9 @@ def _split_repeated_field(field_def: FieldDef,
170
170
  resolved_offset = (_resolve_offset(field_def, env)
171
171
  if offset is None else offset)
172
172
  resolved_size = _resolve_size(field_def, env)
173
- return FieldDef(name=f"{field_def.name}[{index}]",
174
- offset=resolved_offset,
175
- size=resolved_size,
176
- type=field_def.type,
177
- scale=field_def.scale,
178
- repeat=None,
179
- description=field_def.description)
173
+ return field_def.split_repeat(index,
174
+ offset=resolved_offset,
175
+ size=resolved_size)
180
176
 
181
177
 
182
178
  def encode_field(field_def: FieldDef,
@@ -1,8 +1,11 @@
1
1
  """Type definitions for structured layout metadata."""
2
2
  from __future__ import annotations
3
3
 
4
+ import importlib
4
5
  import json
6
+ import re
5
7
  from dataclasses import dataclass, field
8
+ from enum import Enum
6
9
  from typing import Any
7
10
 
8
11
  from sltcore import Info, InfoSize
@@ -25,18 +28,47 @@ class FieldDef:
25
28
  default=None, metadata={"desc": "The repeat count of the field"})
26
29
  description: str | None = field(
27
30
  default=None, metadata={"desc": "The description of the field"})
31
+ range_expression: str | None = field(
32
+ default=None, metadata={"desc": "The value range expression"})
33
+ enum_type: type[Enum] | None = field(default=None,
34
+ metadata={"desc": "The enum type"})
28
35
 
29
36
  def split_repeat(self,
30
37
  index: int,
31
- offset: InfoSize | str | None = None) -> "FieldDef":
38
+ offset: InfoSize | str | None = None,
39
+ size: InfoSize | str | None = None) -> "FieldDef":
32
40
  """Create a single repeated-field definition for the given index."""
33
- return FieldDef(name=f"{self.name}[{index}]",
34
- offset=self.offset if offset is None else offset,
35
- size=self.size,
36
- type=self.type,
41
+ split_name = f"{self.name}[{index}]"
42
+ split_offset_source = self.offset if offset is None else offset
43
+ split_size_source = self.size if size is None else size
44
+ split_offset = self._replace_name_in_expression(split_offset_source,
45
+ self.name, split_name)
46
+ split_size = self._replace_name_in_expression(split_size_source,
47
+ self.name, split_name)
48
+ split_type = self._replace_name_in_expression(self.type, self.name,
49
+ split_name)
50
+ split_range_expression = self._replace_name_in_expression(
51
+ self.range_expression, self.name, split_name)
52
+
53
+ return FieldDef(name=split_name,
54
+ offset=split_offset,
55
+ size=split_size,
56
+ type=split_type,
37
57
  scale=self.scale,
38
58
  repeat=None,
39
- description=self.description)
59
+ description=self.description,
60
+ range_expression=split_range_expression,
61
+ enum_type=self.enum_type)
62
+
63
+ @staticmethod
64
+ def _replace_name_in_expression(value: Any, old_name: str,
65
+ new_name: str) -> Any:
66
+ """Replace standalone old_name tokens in expression-like strings."""
67
+ if not isinstance(value, str):
68
+ return value
69
+ pattern = (rf"(?<![0-9A-Za-z_]){re.escape(old_name)}"
70
+ rf"(?![0-9A-Za-z_])")
71
+ return re.sub(pattern, lambda _: new_name, value)
40
72
 
41
73
  def __lt__(self, other: object) -> bool:
42
74
  """Compare field definitions using a stable serialized sort key."""
@@ -54,6 +86,8 @@ class FieldDef:
54
86
  "scale": self.scale,
55
87
  "repeat": self.repeat,
56
88
  "description": self.description,
89
+ "range_expression": self.range_expression,
90
+ "enum_type": self._serialize_enum_type(self.enum_type),
57
91
  }
58
92
 
59
93
  def to_json(self) -> str:
@@ -71,6 +105,8 @@ class FieldDef:
71
105
  scale=data.get("scale", 1.0),
72
106
  repeat=data.get("repeat"),
73
107
  description=data.get("description"),
108
+ range_expression=data.get("range_expression"),
109
+ enum_type=cls._deserialize_enum_type(data.get("enum_type")),
74
110
  )
75
111
 
76
112
  @classmethod
@@ -124,6 +160,37 @@ class FieldDef:
124
160
  return StructDef.from_dict(value)
125
161
  return value
126
162
 
163
+ @staticmethod
164
+ def _serialize_enum_type(value: type[Enum] | None) -> Any:
165
+ if value is None:
166
+ return None
167
+ return {
168
+ "__type__": "EnumType",
169
+ "module": value.__module__,
170
+ "qualname": value.__qualname__,
171
+ }
172
+
173
+ @classmethod
174
+ def _deserialize_enum_type(cls, value: Any) -> type[Enum] | None:
175
+ if value is None:
176
+ return None
177
+ if not isinstance(value, dict) or value.get("__type__") != "EnumType":
178
+ return None
179
+ module_name = value.get("module")
180
+ qualname = value.get("qualname")
181
+ if not isinstance(module_name, str) or not isinstance(qualname, str):
182
+ return None
183
+ try:
184
+ module = importlib.import_module(module_name)
185
+ enum_type: Any = module
186
+ for attr in qualname.split("."):
187
+ enum_type = getattr(enum_type, attr)
188
+ if isinstance(enum_type, type) and issubclass(enum_type, Enum):
189
+ return enum_type
190
+ except (ImportError, AttributeError, TypeError):
191
+ return None
192
+ return None
193
+
127
194
  def _sort_key(self) -> tuple[Any, ...]:
128
195
  """Build a stable comparison key for ordering field definitions."""
129
196
  return (
@@ -134,6 +201,8 @@ class FieldDef:
134
201
  self.scale,
135
202
  -1 if self.repeat is None else self.repeat,
136
203
  "" if self.description is None else self.description,
204
+ "" if self.range_expression is None else self.range_expression,
205
+ self._sortable_enum_type(self.enum_type),
137
206
  )
138
207
 
139
208
  @staticmethod
@@ -150,6 +219,13 @@ class FieldDef:
150
219
  return (0, json.dumps(value.to_dict(), sort_keys=True))
151
220
  return (1, value)
152
221
 
222
+ @staticmethod
223
+ def _sortable_enum_type(value: type[Enum] | None) -> str:
224
+ """Build a comparable key for enum type metadata."""
225
+ if value is None:
226
+ return ""
227
+ return f"{value.__module__}.{value.__qualname__}"
228
+
153
229
 
154
230
  @dataclass(frozen=True)
155
231
  class FieldInstance:
@@ -1,4 +1,5 @@
1
1
  """Tests for the sltcodec module."""
2
+ from enum import Enum
2
3
  from pathlib import Path
3
4
 
4
5
  import pytest
@@ -10,6 +11,12 @@ from sltcodec import (FieldDef, FieldInstance, StructDef, StructInstance,
10
11
  from sltcodec.codec import decode_field
11
12
 
12
13
 
14
+ class ValueKind(Enum):
15
+ """Enum used for FieldDef enum_type metadata tests."""
16
+ A = 1
17
+ B = 2
18
+
19
+
13
20
  def test_encode_and_decode_round_trip():
14
21
  """Test that encoding and then decoding returns the original data."""
15
22
  struct_def = [
@@ -146,6 +153,48 @@ def test_repeated_field_preserves_description():
146
153
  decoded.field_instances[1].field_def.description == "A repeated value")
147
154
 
148
155
 
156
+ def test_repeated_field_preserves_range_and_enum_metadata():
157
+ """Test repeated fields preserve range/enum metadata on decoded entries."""
158
+ field_def = FieldDef(name="value",
159
+ offset=InfoSize(0, 0),
160
+ size=InfoSize(1, 0),
161
+ type="unsigned int",
162
+ repeat=2,
163
+ range_expression="0 <= value <= 255",
164
+ enum_type=ValueKind)
165
+
166
+ encoded = encode(
167
+ StructInstance(struct_def=StructDef(fields=[field_def]),
168
+ field_instances=[FieldInstance(field_def, [1, 2])]),
169
+ bytearray(),
170
+ )
171
+ decoded = decode([field_def], encoded)
172
+
173
+ assert decoded.field_instances[0].field_def.range_expression == (
174
+ "0 <= value[0] <= 255")
175
+ assert decoded.field_instances[1].field_def.range_expression == (
176
+ "0 <= value[1] <= 255")
177
+ assert decoded.field_instances[0].field_def.enum_type is ValueKind
178
+ assert decoded.field_instances[1].field_def.enum_type is ValueKind
179
+
180
+
181
+ def test_split_repeat_replaces_name_in_range_expression():
182
+ """Test split_repeat replaces field-name tokens in range expressions."""
183
+ field_def = FieldDef(
184
+ name="value",
185
+ offset=InfoSize(0, 0),
186
+ size=InfoSize(1, 0),
187
+ type="unsigned int",
188
+ repeat=2,
189
+ range_expression=("0 <= value and value <= limit and value_count"))
190
+
191
+ split = field_def.split_repeat(1)
192
+
193
+ assert split.name == "value[1]"
194
+ assert split.range_expression == (
195
+ "0 <= value[1] and value[1] <= limit and value_count")
196
+
197
+
149
198
  def test_field_def_json_round_trip():
150
199
  """Test that FieldDef can be serialized and deserialized from JSON."""
151
200
  field_def = FieldDef(name="value",
@@ -169,12 +218,16 @@ def test_field_def_to_json_from_json_round_trip():
169
218
  type="unsigned int",
170
219
  scale=2.0,
171
220
  repeat=3,
172
- description="A repeated value")
221
+ description="A repeated value",
222
+ range_expression="0 <= value <= 255",
223
+ enum_type=ValueKind)
173
224
 
174
225
  payload = field_def.to_json()
175
226
  restored = FieldDef.from_json(payload)
176
227
 
177
228
  assert restored == field_def
229
+ assert restored.range_expression == "0 <= value <= 255"
230
+ assert restored.enum_type is ValueKind
178
231
 
179
232
 
180
233
  def test_struct_def_metadata_json_round_trip():
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