jamcodec 0.1.7__tar.gz → 0.1.8__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.
Files changed (41) hide show
  1. {jamcodec-0.1.7 → jamcodec-0.1.8}/PKG-INFO +1 -1
  2. jamcodec-0.1.8/jamcodec/decoding.py +125 -0
  3. {jamcodec-0.1.7 → jamcodec-0.1.8}/jamcodec/mixins.py +5 -1
  4. {jamcodec-0.1.7 → jamcodec-0.1.8}/jamcodec.egg-info/PKG-INFO +1 -1
  5. {jamcodec-0.1.7 → jamcodec-0.1.8}/jamcodec.egg-info/SOURCES.txt +2 -0
  6. jamcodec-0.1.8/test/test_direct_decode.py +385 -0
  7. {jamcodec-0.1.7 → jamcodec-0.1.8}/LICENSE +0 -0
  8. {jamcodec-0.1.7 → jamcodec-0.1.8}/README.md +0 -0
  9. {jamcodec-0.1.7 → jamcodec-0.1.8}/jamcodec/__init__.py +0 -0
  10. {jamcodec-0.1.7 → jamcodec-0.1.8}/jamcodec/base.py +0 -0
  11. {jamcodec-0.1.7 → jamcodec-0.1.8}/jamcodec/constants.py +0 -0
  12. {jamcodec-0.1.7 → jamcodec-0.1.8}/jamcodec/exceptions.py +0 -0
  13. {jamcodec-0.1.7 → jamcodec-0.1.8}/jamcodec/types.py +0 -0
  14. {jamcodec-0.1.7 → jamcodec-0.1.8}/jamcodec.egg-info/dependency_links.txt +0 -0
  15. {jamcodec-0.1.7 → jamcodec-0.1.8}/jamcodec.egg-info/requires.txt +0 -0
  16. {jamcodec-0.1.7 → jamcodec-0.1.8}/jamcodec.egg-info/top_level.txt +0 -0
  17. {jamcodec-0.1.7 → jamcodec-0.1.8}/pyproject.toml +0 -0
  18. {jamcodec-0.1.7 → jamcodec-0.1.8}/requirements.txt +0 -0
  19. {jamcodec-0.1.7 → jamcodec-0.1.8}/setup.cfg +0 -0
  20. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_array.py +0 -0
  21. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_bitarray.py +0 -0
  22. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_bitvec.py +0 -0
  23. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_boolean.py +0 -0
  24. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_bounded_vec.py +0 -0
  25. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_bytes.py +0 -0
  26. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_collection_buffers.py +0 -0
  27. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_compact.py +0 -0
  28. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_enum.py +0 -0
  29. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_float.py +0 -0
  30. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_hashmap.py +0 -0
  31. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_integer.py +0 -0
  32. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_map.py +0 -0
  33. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_mixins.py +0 -0
  34. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_option.py +0 -0
  35. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_scale_types.py +0 -0
  36. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_scalebytes.py +0 -0
  37. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_string.py +0 -0
  38. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_struct.py +0 -0
  39. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_tuple.py +0 -0
  40. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_varint.py +0 -0
  41. {jamcodec-0.1.7 → jamcodec-0.1.8}/test/test_vec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jamcodec
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Python JAM Codec Library
5
5
  Author-email: JAMdot Technologies <dev@jamdot.tech>
6
6
  Project-URL: Homepage, https://github.com/JAMdotTech/py-jam-codec
@@ -0,0 +1,125 @@
1
+ """Binary model decoding without intermediate serialized values."""
2
+
3
+ import enum
4
+
5
+ from jamcodec.base import JamBytes, JamCodecType, JamCodecTypeDef
6
+ from jamcodec import types
7
+ from jamcodec.exceptions import ScaleDecodeException
8
+
9
+
10
+ class _DecodedValue(JamCodecType):
11
+ # Existing model converters (notably Option) recognize JamCodecType values.
12
+ # Only these two fields are needed; models are built after the whole decode.
13
+ __slots__ = ('type_def', 'value_object')
14
+
15
+ def __init__(self, type_def, value):
16
+ self.type_def = type_def
17
+ self.value_object = value
18
+
19
+
20
+ _WRAPPERS = {
21
+ types.UnsignedInteger: JamCodecType,
22
+ types.SignedInteger: JamCodecType,
23
+ types.Float: JamCodecType,
24
+ types.Bool: JamCodecType,
25
+ types.NullType: JamCodecType,
26
+ types.CompactDef: JamCodecType,
27
+ types.BitVec: JamCodecType,
28
+ types.BitArray: JamCodecType,
29
+ types.BytesDef: JamCodecType,
30
+ types.StringDef: JamCodecType,
31
+ types.VarInt64Def: JamCodecType,
32
+ types.HashDef: types.HashDefObject,
33
+ types.Struct: types.StructObject,
34
+ types.Tuple: JamCodecType,
35
+ types.Enum: JamCodecType,
36
+ types.Option: JamCodecType,
37
+ types.Vec: types.VecType,
38
+ types.BoundedVec: types.VecType,
39
+ types.Array: types.ArrayObject,
40
+ types.Map: JamCodecType,
41
+ }
42
+ _CONTAINERS = frozenset((
43
+ types.Struct, types.Tuple, types.Enum, types.Option,
44
+ types.Vec, types.BoundedVec, types.Array, types.Map,
45
+ ))
46
+
47
+ # A custom hook must still see a normal codec wrapper, including its serialized
48
+ # value and stream offsets. Do not cache plans: definitions remain mutable.
49
+ _HOOK_NAMES = frozenset(('new', 'decode', 'serialize', 'to_serializable_obj', '_check_count'))
50
+ _BUILTINS = {
51
+ cls: (
52
+ wrapper,
53
+ (cls.new, cls.decode, cls.serialize, cls.to_serializable_obj),
54
+ (wrapper.decode, wrapper.serialize, wrapper.to_serializable_obj),
55
+ )
56
+ for cls, wrapper in _WRAPPERS.items()
57
+ }
58
+
59
+
60
+ def _decode_value(codec, data):
61
+ cls = type(codec)
62
+ wrapper, type_methods, wrapper_methods = _BUILTINS.get(cls, (None, None, None))
63
+ if (
64
+ wrapper is None
65
+ or codec.scale_type_cls is not wrapper
66
+ or not _HOOK_NAMES.isdisjoint(codec.__dict__)
67
+ or (cls.new, cls.decode, cls.serialize, cls.to_serializable_obj) != type_methods
68
+ or (wrapper.decode, wrapper.serialize, wrapper.to_serializable_obj) != wrapper_methods
69
+ # Python enums expose their codec tuple rather than constructing a model.
70
+ or (cls is types.Enum and codec._deserialize_type is not None
71
+ and issubclass(codec._deserialize_type, enum.Enum))
72
+ ):
73
+ value = codec.new()
74
+ value.decode(data)
75
+ return value
76
+
77
+ if cls not in _CONTAINERS:
78
+ # Primitive decoders already enforce their wire-format validation.
79
+ value = codec.decode(data)
80
+ elif cls is types.Struct:
81
+ value = {name: _decode_value(child, data) for name, child in codec.arguments.items()}
82
+ elif cls is types.Tuple:
83
+ if len(codec.values) == 1:
84
+ value = _decode_value(codec.values[0], data)
85
+ else:
86
+ value = tuple(_decode_value(child, data) for child in codec.values)
87
+ elif cls is types.Enum:
88
+ index = int.from_bytes(data.get_next_bytes(1), byteorder='little')
89
+ for variant_index, (name, child) in enumerate(codec.variants.items()):
90
+ if type(child) is dict:
91
+ variant_index = child.get('id', variant_index)
92
+ child = child.get('type', child)
93
+ if index == variant_index:
94
+ value = (name, _decode_value(child, data) if child is not None else None)
95
+ break
96
+ else:
97
+ raise ScaleDecodeException(f"Index '{index}' not present in Enum type mapping")
98
+ elif cls is types.Option:
99
+ value = None if data.get_next_bytes(1) == b'\x00' else _decode_value(codec.some, data)
100
+ elif cls in (types.Vec, types.BoundedVec):
101
+ if codec.type_def is types.U8:
102
+ # Byte vectors also delegate serialization hooks to the Bytes codec.
103
+ value = codec.new()
104
+ value.decode(data)
105
+ return value
106
+ start = data.offset
107
+ count = types.VarInt64.decode(data)
108
+ if cls is types.BoundedVec:
109
+ if data.data[start:data.offset] != types.VarInt64.encode(count).data:
110
+ raise ValueError('non-canonical sequence length')
111
+ codec._check_count(count)
112
+ value = [_decode_value(codec.type_def, data) for _ in range(count)]
113
+ elif cls is types.Array:
114
+ value = (codec.decode(data) if codec.type_def is types.U8 else
115
+ [_decode_value(codec.type_def, data) for _ in range(codec.length)])
116
+ elif cls is types.Map:
117
+ count = types.VarInt64.decode(data)
118
+ value = [(_decode_value(codec.key_def, data), _decode_value(codec.value_def, data))
119
+ for _ in range(count)]
120
+ return _DecodedValue(codec, value)
121
+
122
+
123
+ def decode_to_object(codec: JamCodecTypeDef, data: JamBytes):
124
+ """Decode a model while preserving the normal wrapper path for custom codecs."""
125
+ return _decode_value(codec, data).to_serializable_obj()
@@ -7,6 +7,7 @@ import json
7
7
 
8
8
  from jamcodec.base import JamCodecTypeDef, JamCodecType, JamBytes
9
9
  from jamcodec.types import Struct, StructObject, Option, Vec, Enum
10
+ from jamcodec.decoding import decode_to_object
10
11
 
11
12
  T = TypeVar('T')
12
13
 
@@ -77,7 +78,10 @@ class Serializable:
77
78
 
78
79
  @classmethod
79
80
  def from_jam_bytes(cls: Type[T], scale_bytes: JamBytes) -> T:
80
- scale_obj = cls.to_codec_def().new()
81
+ codec = cls.to_codec_def()
82
+ if getattr(cls.from_codec_type, '__func__', None) is Serializable.from_codec_type.__func__:
83
+ return decode_to_object(codec, scale_bytes)
84
+ scale_obj = codec.new()
81
85
  scale_obj.decode(scale_bytes)
82
86
  return cls.from_codec_type(scale_obj)
83
87
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jamcodec
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Python JAM Codec Library
5
5
  Author-email: JAMdot Technologies <dev@jamdot.tech>
6
6
  Project-URL: Homepage, https://github.com/JAMdotTech/py-jam-codec
@@ -5,6 +5,7 @@ requirements.txt
5
5
  jamcodec/__init__.py
6
6
  jamcodec/base.py
7
7
  jamcodec/constants.py
8
+ jamcodec/decoding.py
8
9
  jamcodec/exceptions.py
9
10
  jamcodec/mixins.py
10
11
  jamcodec/types.py
@@ -21,6 +22,7 @@ test/test_bounded_vec.py
21
22
  test/test_bytes.py
22
23
  test/test_collection_buffers.py
23
24
  test/test_compact.py
25
+ test/test_direct_decode.py
24
26
  test/test_enum.py
25
27
  test/test_float.py
26
28
  test/test_hashmap.py
@@ -0,0 +1,385 @@
1
+ import enum
2
+ import unittest
3
+ from dataclasses import dataclass, field
4
+ from types import MethodType
5
+ from unittest.mock import patch
6
+
7
+ from jamcodec.base import JamBytes, JamCodecType, JamCodecTypeDef
8
+ from jamcodec.decoding import decode_to_object
9
+ from jamcodec.exceptions import RemainingScaleBytesNotEmptyException, ScaleSerializeException
10
+ from jamcodec.mixins import Serializable
11
+ from jamcodec.types import (
12
+ Array, Bool, BoundedVec, Bytes, Enum, H256, Map, Null, Option,
13
+ String, Struct, StructObject, Tuple, U8, U16, UnsignedInteger, Vec,
14
+ )
15
+
16
+
17
+ def decode_with_wrappers(codec, data):
18
+ """The original binary-to-model route, independent of the optimized mixin."""
19
+ wrapper = codec.new()
20
+ wrapper.decode(data)
21
+ return wrapper.to_serializable_obj()
22
+
23
+
24
+ @dataclass
25
+ class Item(Serializable):
26
+ number: int = field(metadata={'codec': U16})
27
+ payload: bytes = field(metadata={'codec': Bytes})
28
+
29
+
30
+ @dataclass
31
+ class Packet(Serializable):
32
+ items: list = field(metadata={'codec': BoundedVec(Item.to_codec_def(), 3)})
33
+ optional: object = field(metadata={'codec': Option(Item.to_codec_def())})
34
+ lookup: dict = field(metadata={'codec': Map(U8, Option(Item.to_codec_def()))})
35
+ pair: tuple = field(metadata={'codec': Tuple(U8, Bytes)})
36
+ numbers: list = field(metadata={'codec': Array(U16, 2)})
37
+ fixed: bytes = field(metadata={'codec': Array(U8, 2)})
38
+ digest: bytes = field(metadata={'codec': H256})
39
+
40
+
41
+ @dataclass
42
+ class Choice(Serializable):
43
+ item: object = None
44
+ empty: object = None
45
+ _codec_type_def = Enum(item={'id': 19, 'type': Item.to_codec_def()}, empty={'id': 255, 'type': Null})
46
+
47
+
48
+ class Status(Serializable, enum.Enum):
49
+ ready = 10
50
+ finished = 20
51
+
52
+
53
+ class TestDirectDecode(unittest.TestCase):
54
+ def assertEquivalent(self, codec, payload):
55
+ """Compare values, exact Python types, errors, and stream consumption."""
56
+ expected_data = JamBytes(b'\x99' + payload + b'\x42')
57
+ actual_data = JamBytes(expected_data.to_bytes())
58
+ expected_data.offset = actual_data.offset = 1
59
+ try:
60
+ expected = decode_with_wrappers(codec, expected_data)
61
+ except Exception as error:
62
+ with self.assertRaises(type(error)) as caught:
63
+ decode_to_object(codec, actual_data)
64
+ self.assertEqual(str(error), str(caught.exception))
65
+ else:
66
+ actual = decode_to_object(codec, actual_data)
67
+ self.assertEqual(expected, actual)
68
+ self.assertSameTypes(expected, actual)
69
+ self.assertEqual(expected_data.offset, actual_data.offset)
70
+
71
+ def assertSameTypes(self, expected, actual):
72
+ self.assertIs(type(expected), type(actual))
73
+ if isinstance(expected, dict):
74
+ for key in expected:
75
+ self.assertSameTypes(expected[key], actual[key])
76
+ elif isinstance(expected, (list, tuple)):
77
+ for left, right in zip(expected, actual):
78
+ self.assertSameTypes(left, right)
79
+
80
+ def test_nested_model_and_collection_parity(self):
81
+ value = Packet(
82
+ items=[Item(1, b'first'), Item(258, b'')],
83
+ optional=Item(65535, b'optional'),
84
+ lookup={2: None, 1: Item(7, b'value')},
85
+ pair=(3, b'pair'), numbers=[1, 258], fixed=b'ab', digest=bytes(range(32)),
86
+ )
87
+ payload = value.to_jam_bytes().to_bytes()
88
+ self.assertEquivalent(Packet.to_codec_def(), payload)
89
+ stream = JamBytes(b'\x99' + payload + b'\x42')
90
+ stream.offset = 1
91
+ decoded = Packet.from_jam_bytes(stream)
92
+ self.assertEqual(value, decoded)
93
+ self.assertIs(type(decoded.fixed), bytearray)
94
+ self.assertEqual(bytearray(b'\x42'), stream.get_remaining_bytes())
95
+
96
+ def test_leaf_values_and_byte_collection_types(self):
97
+ cases = (
98
+ (Null, b''), (Bool(), b'\x01'), (Bytes, b'\x02ab'),
99
+ (String, b'\x02ab'), (String, b'\x01\xff'), (H256, bytes(32)),
100
+ (Array(U8, 2), b'ab'), (Array(U16, 2), b'\x01\x00\x02\x00'),
101
+ (Vec(U16), b'\x02\x01\x00\x02\x00'),
102
+ (BoundedVec(U8, 2), b'\x02ab'),
103
+ (Option(Null), b'\x01'), (Option(U16), b'\x00'),
104
+ # Existing Option accepts every nonzero tag as Some.
105
+ (Option(U16), b'\x02\x01\x00'),
106
+ )
107
+ for codec, payload in cases:
108
+ with self.subTest(codec=type(codec).__name__, payload=payload):
109
+ self.assertEquivalent(codec, payload)
110
+ self.assertIs(Null, decode_to_object(Null, JamBytes(b'')))
111
+
112
+ def test_enum_model_and_explicit_discriminants(self):
113
+ for value in (Choice(item=Item(258, b'x')), Choice(empty=Null)):
114
+ with self.subTest(value=value):
115
+ self.assertEquivalent(Choice.to_codec_def(), value.to_jam_bytes().to_bytes())
116
+ self.assertEqual(value, Choice.from_jam_bytes(value.to_jam_bytes()))
117
+ self.assertEqual(('finished', None), Status.from_jam_bytes(JamBytes(b'\x01')))
118
+ self.assertEquivalent(Status.to_codec_def(), b'\x00')
119
+ self.assertEquivalent(Choice.to_codec_def(), b'\xfe')
120
+
121
+ def test_preserves_existing_single_tuple_and_byte_vec_behavior(self):
122
+ # These routes have unusual legacy behavior; optimizing decode must
123
+ # preserve it rather than silently change their model representation.
124
+ for codec, payload in ((Tuple(U8), b'\x03'), (Vec(U8), b'\x02ab')):
125
+ with self.subTest(codec=type(codec).__name__):
126
+ self.assertEquivalent(codec, payload)
127
+
128
+ def test_map_duplicate_keys_keep_last_value(self):
129
+ codec = Map(U8, U16)
130
+ payload = b'\x02\x03\x01\x00\x03\x02\x00'
131
+ self.assertEquivalent(codec, payload)
132
+ self.assertEqual({3: 2}, decode_to_object(codec, JamBytes(payload)))
133
+
134
+ def test_truncated_or_invalid_data_preserves_errors_and_cursor(self):
135
+ cases = (
136
+ (Bool(), b'\x02'), (U16, b''), (Bytes, b'\x05a'),
137
+ (Array(U8, 3), b'a'), (Vec(U16), b'\x02\x01\x00'),
138
+ (Map(U8, U16), b'\x01\x01'),
139
+ (Item.to_codec_def(), b'\x01\x00\x03a'),
140
+ (Choice.to_codec_def(), b'\x13\x01'),
141
+ (BoundedVec(U16, 3), b'\x80'),
142
+ )
143
+ for codec, payload in cases:
144
+ with self.subTest(codec=type(codec).__name__, payload=payload):
145
+ self.assertEquivalent(codec, payload)
146
+
147
+ def test_bounded_vec_checks_length_before_creating_elements(self):
148
+ class NoElementAllocation(JamCodecTypeDef):
149
+ def new(self, **kwargs):
150
+ raise AssertionError('Invalid count must be rejected first')
151
+
152
+ codec = BoundedVec(NoElementAllocation(), maximum=4, minimum=2, multiple=2)
153
+ for payload in (b'\x00', b'\x01', b'\x03', b'\x05', b'\x80\x02', b'\xff' + b'\xff' * 8):
154
+ with self.subTest(payload=payload):
155
+ self.assertEquivalent(codec, payload)
156
+ with self.assertRaises(ValueError):
157
+ decode_to_object(codec, JamBytes(payload))
158
+
159
+ def test_bounded_vec_inclusive_limits_and_nested_bytes(self):
160
+ codec = BoundedVec(U16, maximum=4, minimum=2, multiple=2)
161
+ for count in (2, 4):
162
+ self.assertEquivalent(codec, codec.encode(list(range(count))).to_bytes())
163
+ nested = BoundedVec(BoundedVec(U8, maximum=4, minimum=2, multiple=2), maximum=2)
164
+ payload = nested.encode([b'ab', b'cdef']).to_bytes()
165
+ self.assertEquivalent(nested, payload)
166
+ self.assertEqual([b'ab', b'cdef'], decode_to_object(nested, JamBytes(payload)))
167
+
168
+ def test_custom_type_definition_hooks_are_preserved(self):
169
+ events = []
170
+
171
+ class CustomByte(UnsignedInteger):
172
+ def decode(self, data):
173
+ events.append('decode')
174
+ return super().decode(data) + 1
175
+
176
+ def serialize(self, value):
177
+ events.append('serialize')
178
+ if value == 256:
179
+ raise ScaleSerializeException('custom serialization validation')
180
+ return super().serialize(value)
181
+
182
+ def to_serializable_obj(self, value):
183
+ events.append('to_object')
184
+ return value + 100
185
+
186
+ codec = CustomByte(8)
187
+ self.assertEquivalent(Vec(codec), b'\x01\x02')
188
+ events.clear()
189
+ self.assertEqual(103, decode_to_object(codec, JamBytes(b'\x02')))
190
+ self.assertEqual(['decode', 'serialize', 'to_object'], events)
191
+ self.assertEquivalent(codec, b'\xff')
192
+
193
+ def test_custom_wrapper_hooks_and_impl_updates_are_preserved(self):
194
+ events = []
195
+
196
+ class CustomWrapper(StructObject):
197
+ def decode(self, data, check_remaining=False):
198
+ events.append('decode')
199
+ return super().decode(data, check_remaining)
200
+
201
+ def serialize(self):
202
+ events.append('serialize')
203
+ return super().serialize()
204
+
205
+ def to_serializable_obj(self):
206
+ events.append('to_object')
207
+ value = super().to_serializable_obj()
208
+ value.number += 100
209
+ return value
210
+
211
+ @dataclass
212
+ class CustomPacket(Serializable):
213
+ number: int
214
+ _codec_type_def = Struct(number=U8)
215
+
216
+ self.assertEqual(CustomPacket(2), CustomPacket.from_jam_bytes(JamBytes(b'\x02')))
217
+ CustomPacket.to_codec_def().impl(CustomWrapper)
218
+ events.clear()
219
+ self.assertEqual(CustomPacket(102), CustomPacket.from_jam_bytes(JamBytes(b'\x02')))
220
+ self.assertEqual(['decode', 'serialize', 'to_object'], events)
221
+ self.assertEquivalent(CustomPacket.to_codec_def(), b'\x02')
222
+ nested = Vec(CustomPacket.to_codec_def())
223
+ self.assertEqual([CustomPacket(102)], decode_to_object(nested, JamBytes(b'\x01\x02')))
224
+
225
+ def test_custom_from_codec_type_runs_only_at_root(self):
226
+ @dataclass
227
+ class Adjusted(Serializable):
228
+ number: int = field(metadata={'codec': U8})
229
+
230
+ @classmethod
231
+ def from_codec_type(cls, wrapper):
232
+ value = super().from_codec_type(wrapper)
233
+ value.number += 100
234
+ return value
235
+
236
+ @dataclass
237
+ class Container(Serializable):
238
+ value: Adjusted = field(metadata={'codec': Adjusted.to_codec_def()})
239
+
240
+ self.assertEqual(Adjusted(102), Adjusted.from_jam_bytes(JamBytes(b'\x02')))
241
+ self.assertEqual(Container(Adjusted(2)), Container.from_jam_bytes(JamBytes(b'\x02')))
242
+
243
+ def test_nested_model_validation_runs_after_decoding_the_root(self):
244
+ @dataclass
245
+ class ValidatedItem(Serializable):
246
+ number: int = field(metadata={'codec': U8})
247
+
248
+ def __post_init__(self):
249
+ if self.number == 0:
250
+ raise ValueError('number must be positive')
251
+
252
+ @dataclass
253
+ class Envelope(Serializable):
254
+ item: ValidatedItem = field(metadata={'codec': ValidatedItem.to_codec_def()})
255
+ tail: int = field(metadata={'codec': U16})
256
+
257
+ # The original route consumes the tail before model construction fails.
258
+ self.assertEquivalent(Envelope.to_codec_def(), b'\x00\x02\x01')
259
+
260
+ def test_nested_custom_conversion_runs_after_decoding_the_root(self):
261
+ class RejectedByte(UnsignedInteger):
262
+ def to_serializable_obj(self, value):
263
+ raise ValueError('custom conversion rejection')
264
+
265
+ @dataclass
266
+ class Envelope(Serializable):
267
+ item: int = field(metadata={'codec': RejectedByte(8)})
268
+ tail: int = field(metadata={'codec': U16})
269
+
270
+ self.assertEquivalent(Envelope.to_codec_def(), b'\x01\x02\x01')
271
+
272
+ def test_decoded_results_and_input_buffers_remain_independent(self):
273
+ codec = BoundedVec(Array(U8, 2), maximum=2)
274
+ first_data = JamBytes(b'\x02abcd')
275
+ first = decode_to_object(codec, first_data)
276
+ second = decode_to_object(codec, JamBytes(b'\x02abcd'))
277
+ first[0][0] = ord('z')
278
+ first.append(bytearray(b'ef'))
279
+ first_data.data[1] = ord('y')
280
+ self.assertEqual([bytearray(b'ab'), bytearray(b'cd')], second)
281
+ self.assertEqual(bytearray(b'zb'), first[0])
282
+
283
+ def test_mutable_codec_definitions_are_not_frozen_by_first_decode(self):
284
+ @dataclass
285
+ class MutablePacket(Serializable):
286
+ number: int
287
+ _codec_type_def = Struct(number=U8)
288
+
289
+ self.assertEqual(MutablePacket(1), MutablePacket.from_jam_bytes(JamBytes(b'\x01')))
290
+ codec = MutablePacket.to_codec_def()
291
+ codec.arguments['number'] = U16
292
+ self.assertEqual(MutablePacket(258), MutablePacket.from_jam_bytes(JamBytes(b'\x02\x01')))
293
+ self.assertEquivalent(codec, b'\x02\x01')
294
+
295
+ def test_builtin_instance_hooks_are_preserved(self):
296
+ for hook in ('decode', 'serialize', 'to_serializable_obj'):
297
+ with self.subTest(hook=hook):
298
+ codec = UnsignedInteger(8)
299
+ original = getattr(codec, hook)
300
+ calls = []
301
+
302
+ def custom(self, value):
303
+ calls.append(hook)
304
+ return original(value) + 10
305
+
306
+ setattr(codec, hook, MethodType(custom, codec))
307
+ self.assertEquivalent(codec, b'\x02')
308
+ calls.clear()
309
+ decoded = decode_to_object(codec, JamBytes(b'\x02'))
310
+ # Serialization is a validation hook; the model value still
311
+ # comes from decode and to_serializable_obj.
312
+ self.assertEqual(2 if hook == 'serialize' else 12, decoded)
313
+ self.assertEqual([hook], calls)
314
+
315
+ def test_builtin_factory_updates_after_first_decode_are_preserved(self):
316
+ class AdjustedWrapper(JamCodecType):
317
+ def to_serializable_obj(self):
318
+ return super().to_serializable_obj() + 100
319
+
320
+ codec = UnsignedInteger(8)
321
+ self.assertEqual(2, decode_to_object(codec, JamBytes(b'\x02')))
322
+ calls = []
323
+
324
+ def custom_factory(self, **kwargs):
325
+ calls.append('new')
326
+ return AdjustedWrapper(type_def=self, **kwargs)
327
+
328
+ codec.new = MethodType(custom_factory, codec)
329
+ self.assertEqual(102, decode_to_object(codec, JamBytes(b'\x02')))
330
+ self.assertEqual(['new'], calls)
331
+ self.assertEquivalent(codec, b'\x02')
332
+
333
+ def test_builtin_class_serialization_validation_is_preserved(self):
334
+ original = UnsignedInteger.serialize
335
+
336
+ def checked(self, value):
337
+ if value == 255:
338
+ raise ScaleSerializeException('class serialization validation')
339
+ return original(self, value)
340
+
341
+ with patch.object(UnsignedInteger, 'serialize', checked):
342
+ self.assertEquivalent(Vec(U16), b'\x01\xff\x00')
343
+ with self.assertRaisesRegex(ScaleSerializeException, 'class serialization validation'):
344
+ decode_to_object(U8, JamBytes(b'\xff'))
345
+
346
+ def test_byte_vectors_preserve_bytes_serialization_validation(self):
347
+ def rejected(value):
348
+ raise ScaleSerializeException('byte vector serialization validation')
349
+
350
+ with patch.object(Bytes, 'serialize', rejected):
351
+ for codec in (Vec(U8), BoundedVec(U8, maximum=2)):
352
+ with self.subTest(codec=type(codec).__name__):
353
+ self.assertEquivalent(codec, b'\x02ab')
354
+ with self.assertRaisesRegex(ScaleSerializeException, 'byte vector serialization validation'):
355
+ decode_to_object(codec, JamBytes(b'\x02ab'))
356
+
357
+ def test_truncated_trailing_field_prevents_nested_model_construction(self):
358
+ constructed = []
359
+
360
+ @dataclass
361
+ class ObservedItem(Serializable):
362
+ number: int = field(metadata={'codec': U8})
363
+
364
+ def __post_init__(self):
365
+ constructed.append(self.number)
366
+
367
+ @dataclass
368
+ class Envelope(Serializable):
369
+ item: ObservedItem = field(metadata={'codec': ObservedItem.to_codec_def()})
370
+ tail: int = field(metadata={'codec': U16})
371
+
372
+ codec = Envelope.to_codec_def()
373
+ # Even though the item is complete, construction must wait until the
374
+ # entire root has decoded successfully.
375
+ for decode in (decode_with_wrappers, decode_to_object):
376
+ with self.subTest(decode=decode.__name__):
377
+ stream = JamBytes(b'\x01\x02')
378
+ with self.assertRaises(RemainingScaleBytesNotEmptyException):
379
+ decode(codec, stream)
380
+ self.assertEqual(1, stream.offset)
381
+ self.assertEqual([], constructed)
382
+
383
+
384
+ if __name__ == '__main__':
385
+ unittest.main()
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
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
File without changes