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.
- {sltcodec-0.2.0 → sltcodec-0.3.0}/PKG-INFO +2 -2
- {sltcodec-0.2.0 → sltcodec-0.3.0}/pyproject.toml +1 -1
- {sltcodec-0.2.0 → sltcodec-0.3.0}/src/sltcodec/codec.py +3 -7
- {sltcodec-0.2.0 → sltcodec-0.3.0}/src/sltcodec/types.py +82 -6
- {sltcodec-0.2.0 → sltcodec-0.3.0}/tests/test_codec.py +54 -1
- {sltcodec-0.2.0 → sltcodec-0.3.0}/.github/workflows/publish_to_pypi.yml +0 -0
- {sltcodec-0.2.0 → sltcodec-0.3.0}/.github/workflows/publish_to_testpypi.yml +0 -0
- {sltcodec-0.2.0 → sltcodec-0.3.0}/.gitignore +0 -0
- {sltcodec-0.2.0 → sltcodec-0.3.0}/.python-version +0 -0
- {sltcodec-0.2.0 → sltcodec-0.3.0}/.vscode/launch.json +0 -0
- {sltcodec-0.2.0 → sltcodec-0.3.0}/.vscode/settings.json +0 -0
- {sltcodec-0.2.0 → sltcodec-0.3.0}/LICENSE +0 -0
- {sltcodec-0.2.0 → sltcodec-0.3.0}/README.md +0 -0
- {sltcodec-0.2.0 → sltcodec-0.3.0}/bump_major.ps1 +0 -0
- {sltcodec-0.2.0 → sltcodec-0.3.0}/bump_minor.ps1 +0 -0
- {sltcodec-0.2.0 → sltcodec-0.3.0}/bump_patch.ps1 +0 -0
- {sltcodec-0.2.0 → sltcodec-0.3.0}/src/sltcodec/__init__.py +0 -0
- {sltcodec-0.2.0 → sltcodec-0.3.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: sltcodec
|
|
3
|
-
Version: 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
|
|
@@ -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
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|