jamcodec 0.1.8__tar.gz → 0.1.9__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 (43) hide show
  1. {jamcodec-0.1.8 → jamcodec-0.1.9}/PKG-INFO +1 -1
  2. jamcodec-0.1.9/jamcodec/encoding.py +383 -0
  3. {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/mixins.py +15 -1
  4. {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec.egg-info/PKG-INFO +1 -1
  5. {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec.egg-info/SOURCES.txt +2 -0
  6. jamcodec-0.1.9/test/test_direct_encode.py +440 -0
  7. {jamcodec-0.1.8 → jamcodec-0.1.9}/LICENSE +0 -0
  8. {jamcodec-0.1.8 → jamcodec-0.1.9}/README.md +0 -0
  9. {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/__init__.py +0 -0
  10. {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/base.py +0 -0
  11. {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/constants.py +0 -0
  12. {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/decoding.py +0 -0
  13. {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/exceptions.py +0 -0
  14. {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/types.py +0 -0
  15. {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec.egg-info/dependency_links.txt +0 -0
  16. {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec.egg-info/requires.txt +0 -0
  17. {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec.egg-info/top_level.txt +0 -0
  18. {jamcodec-0.1.8 → jamcodec-0.1.9}/pyproject.toml +0 -0
  19. {jamcodec-0.1.8 → jamcodec-0.1.9}/requirements.txt +0 -0
  20. {jamcodec-0.1.8 → jamcodec-0.1.9}/setup.cfg +0 -0
  21. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_array.py +0 -0
  22. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_bitarray.py +0 -0
  23. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_bitvec.py +0 -0
  24. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_boolean.py +0 -0
  25. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_bounded_vec.py +0 -0
  26. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_bytes.py +0 -0
  27. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_collection_buffers.py +0 -0
  28. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_compact.py +0 -0
  29. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_direct_decode.py +0 -0
  30. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_enum.py +0 -0
  31. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_float.py +0 -0
  32. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_hashmap.py +0 -0
  33. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_integer.py +0 -0
  34. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_map.py +0 -0
  35. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_mixins.py +0 -0
  36. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_option.py +0 -0
  37. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_scale_types.py +0 -0
  38. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_scalebytes.py +0 -0
  39. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_string.py +0 -0
  40. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_struct.py +0 -0
  41. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_tuple.py +0 -0
  42. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_varint.py +0 -0
  43. {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_vec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jamcodec
3
- Version: 0.1.8
3
+ Version: 0.1.9
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,383 @@
1
+ """Prepare standard model values for binary encoding without codec wrappers."""
2
+
3
+ import dataclasses
4
+ import struct
5
+ from types import GetSetDescriptorType
6
+
7
+ from jamcodec import types
8
+ from jamcodec.base import JamBytes, JamCodecType
9
+
10
+
11
+ _WRAPPERS = {
12
+ types.UnsignedInteger: JamCodecType,
13
+ types.SignedInteger: JamCodecType,
14
+ types.Float: JamCodecType,
15
+ types.Bool: JamCodecType,
16
+ types.NullType: JamCodecType,
17
+ types.CompactDef: JamCodecType,
18
+ types.BitVec: JamCodecType,
19
+ types.BitArray: JamCodecType,
20
+ types.BytesDef: JamCodecType,
21
+ types.StringDef: JamCodecType,
22
+ types.VarInt64Def: JamCodecType,
23
+ types.HashDef: types.HashDefObject,
24
+ types.Struct: types.StructObject,
25
+ types.Tuple: JamCodecType,
26
+ types.Enum: JamCodecType,
27
+ types.Option: JamCodecType,
28
+ types.Vec: types.VecType,
29
+ types.BoundedVec: types.VecType,
30
+ types.Array: types.ArrayObject,
31
+ types.Map: JamCodecType,
32
+ }
33
+ _TYPE_METHODS = {
34
+ cls: (cls.new, cls.deserialize, cls.serialize, cls.encode,
35
+ getattr(cls, '_check_count', None))
36
+ for cls in _WRAPPERS
37
+ }
38
+ _WRAPPER_METHODS = {
39
+ cls: (cls.__init__, cls.deserialize, cls.serialize, cls.encode,
40
+ cls.__getattribute__, cls.__setattr__, cls.value)
41
+ for cls in set(_WRAPPERS.values())
42
+ }
43
+ _HOOK_NAMES = frozenset(('new', 'deserialize', 'serialize', 'encode', '_check_count'))
44
+ _PRIMITIVES = frozenset(_WRAPPERS) - {
45
+ types.Struct, types.Tuple, types.Enum, types.Option,
46
+ types.Vec, types.BoundedVec, types.Array, types.Map,
47
+ }
48
+ _INTEGERS = frozenset((types.UnsignedInteger, types.SignedInteger, types.CompactDef, types.VarInt64Def))
49
+ _MODEL_HOOKS = frozenset(('__getattr__', '__bool__', '__len__'))
50
+
51
+
52
+ class _UseWrappers(Exception):
53
+ pass
54
+
55
+
56
+ class _Encoder:
57
+ def __init__(self):
58
+ self.checked = set()
59
+ self.checked_types = set()
60
+ self.checked_wrappers = set()
61
+ self.model_fields = {}
62
+ self.active = set()
63
+ self.data = bytearray()
64
+
65
+ def check(self, codec):
66
+ cls = type(codec)
67
+ if id(codec) in self.checked:
68
+ return cls
69
+ wrapper = _WRAPPERS.get(cls)
70
+ if (wrapper is None or codec.scale_type_cls is not wrapper
71
+ or not _HOOK_NAMES.isdisjoint(codec.__dict__)):
72
+ raise _UseWrappers
73
+ if cls not in self.checked_types:
74
+ if (cls.new, cls.deserialize, cls.serialize, cls.encode,
75
+ getattr(cls, '_check_count', None)) != _TYPE_METHODS[cls]:
76
+ raise _UseWrappers
77
+ self.checked_types.add(cls)
78
+ if wrapper not in self.checked_wrappers:
79
+ if (wrapper.__init__, wrapper.deserialize, wrapper.serialize, wrapper.encode,
80
+ wrapper.__getattribute__, wrapper.__setattr__, wrapper.value) != _WRAPPER_METHODS[wrapper]:
81
+ raise _UseWrappers
82
+ self.checked_wrappers.add(wrapper)
83
+
84
+ if cls is types.Struct:
85
+ if type(codec.arguments) is not dict or any(type(name) is not str for name in codec.arguments):
86
+ raise _UseWrappers
87
+ elif cls is types.Enum:
88
+ if (type(codec.variants) is not dict or
89
+ (codec._deserialize_type is not None and type(codec._deserialize_type) is not type)):
90
+ raise _UseWrappers
91
+ if any(type(name) is not str for name in codec.variants):
92
+ raise _UseWrappers
93
+ elif cls is types.Tuple:
94
+ if type(codec.values) not in (list, tuple):
95
+ raise _UseWrappers
96
+ elif cls in (types.Vec, types.BoundedVec, types.Array):
97
+ if cls is types.Array and type(codec.length) is not int:
98
+ raise _UseWrappers
99
+ if cls is types.BoundedVec and any(type(bound) is not int for bound in (
100
+ codec.minimum, codec.maximum, codec.multiple,
101
+ )):
102
+ raise _UseWrappers
103
+ if cls in (types.Vec, types.BoundedVec):
104
+ self.check(types.VarInt64)
105
+ if codec.type_def is types.U8:
106
+ self.check(types.Bytes)
107
+ elif cls is types.Map:
108
+ self.check(types.VarInt64)
109
+ elif cls in (types.BytesDef, types.StringDef, types.BitVec):
110
+ self.check(types.VarInt64)
111
+ elif cls in (types.UnsignedInteger, types.SignedInteger, types.HashDef):
112
+ if (type(codec.byte_count) is not int
113
+ or type(codec.bits) is not int):
114
+ raise _UseWrappers
115
+ elif cls is types.Float:
116
+ if type(codec.struct_format) is not str:
117
+ raise _UseWrappers
118
+ elif cls is types.BitArray:
119
+ if type(codec.length) is not int or type(codec.strict_decoding) is not bool:
120
+ raise _UseWrappers
121
+ self.checked.add(id(codec))
122
+ return cls
123
+
124
+ def fields(self, value):
125
+ """Read plain dataclass fields without invoking user-defined accessors."""
126
+ cls = type(value)
127
+ if type(cls) is not type or not dataclasses.is_dataclass(cls):
128
+ raise _UseWrappers
129
+ if cls not in self.model_fields:
130
+ if (cls.__getattribute__ is not object.__getattribute__ or
131
+ issubclass(cls, JamCodecType)):
132
+ raise _UseWrappers
133
+ names = tuple(field.name for field in dataclasses.fields(cls))
134
+ remaining = set(names)
135
+ dictionary = None
136
+ for base in cls.__mro__:
137
+ attributes = vars(base)
138
+ if not _MODEL_HOOKS.isdisjoint(attributes):
139
+ raise _UseWrappers
140
+ if dictionary is None and '__dict__' in attributes:
141
+ dictionary = attributes['__dict__']
142
+ for name in remaining.intersection(attributes):
143
+ if hasattr(type(attributes[name]), '__get__'):
144
+ raise _UseWrappers
145
+ remaining.remove(name)
146
+ if type(dictionary) is not GetSetDescriptorType:
147
+ raise _UseWrappers
148
+ self.model_fields[cls] = names
149
+ values = object.__getattribute__(value, '__dict__')
150
+ names = self.model_fields[cls]
151
+ if any(name not in values for name in names):
152
+ raise _UseWrappers
153
+ return values if len(values) == len(names) else {name: values[name] for name in names}
154
+
155
+ def plain_leaf(self, value):
156
+ cls = type(value)
157
+ if cls in (bytes, bytearray, int, float, bool, str, type(None)):
158
+ return
159
+ if value is types.Null and not any(
160
+ name in base.__dict__ for base in cls.__mro__ for name in ('__bool__', '__len__')
161
+ ):
162
+ return
163
+ if id(value) in self.active:
164
+ raise _UseWrappers
165
+ self.active.add(id(value))
166
+ try:
167
+ if cls in (list, tuple):
168
+ for item in value:
169
+ self.plain_leaf(item)
170
+ elif cls is dict:
171
+ for key, item in value.items():
172
+ self.plain_leaf(key)
173
+ self.plain_leaf(item)
174
+ else:
175
+ raise _UseWrappers
176
+ finally:
177
+ self.active.remove(id(value))
178
+
179
+ def primitive(self, codec, cls, value, native_bytes):
180
+ value_type = type(value)
181
+ if cls in _INTEGERS:
182
+ if value_type is not int:
183
+ raise _UseWrappers
184
+ return value
185
+ if cls is types.Float:
186
+ if value_type is not float:
187
+ raise _UseWrappers
188
+ return value
189
+ if cls is types.Bool:
190
+ if value_type is not bool:
191
+ raise _UseWrappers
192
+ return value
193
+ if cls in (types.BytesDef, types.HashDef):
194
+ if value_type is bytearray:
195
+ value = bytes(value)
196
+ value_type = bytes
197
+ if value_type is not bytes:
198
+ raise _UseWrappers
199
+ if cls is types.HashDef and len(value) != codec.byte_count:
200
+ raise _UseWrappers
201
+ return value if native_bytes else codec.serialize(value)
202
+ if cls is types.StringDef:
203
+ if value_type is not str:
204
+ raise _UseWrappers
205
+ return value
206
+ # Less common leaves retain their existing normalization and validation.
207
+ self.plain_leaf(value)
208
+ return codec.serialize(codec.deserialize(value))
209
+
210
+ def length(self, count):
211
+ if count < 128:
212
+ self.data.append(count)
213
+ else:
214
+ self.data.extend(types.VarInt64.encode(count).data)
215
+
216
+ def prepare(self, codec, value, emit=True, native_bytes=True):
217
+ """Write plain inputs, or return serialized values for map sorting.
218
+
219
+ When emitting a container, only its None/non-None result matters to
220
+ Option; its bytes have already been appended to the output buffer.
221
+ """
222
+ cls = self.check(codec)
223
+ if cls in _PRIMITIVES:
224
+ result = self.primitive(codec, cls, value, native_bytes)
225
+ if emit:
226
+ if cls is types.HashDef:
227
+ self.data.extend(result)
228
+ elif cls is types.UnsignedInteger:
229
+ self.data.extend(result.to_bytes(codec.byte_count, 'little'))
230
+ elif cls is types.BytesDef:
231
+ self.length(len(result))
232
+ self.data.extend(result)
233
+ elif cls is types.Bool:
234
+ self.data.append(result)
235
+ elif cls is types.VarInt64Def and 0 <= result < 128:
236
+ self.data.append(result)
237
+ elif cls is not types.NullType:
238
+ self.data.extend(codec.encode(result).data)
239
+ return result
240
+ if cls is types.Struct:
241
+ if type(value) is not dict:
242
+ value = self.fields(value)
243
+ elif any(type(name) is not str for name in value):
244
+ raise _UseWrappers
245
+ result = {} if not emit else True
246
+ for name, child in codec.arguments.items():
247
+ if name not in value:
248
+ raise _UseWrappers
249
+ item = self.prepare(child, value[name], emit, native_bytes)
250
+ if not emit:
251
+ result[name] = item
252
+ return result
253
+ if cls is types.Tuple:
254
+ if type(value) not in (tuple, list) or len(value) < len(codec.values):
255
+ raise _UseWrappers
256
+ result = [self.prepare(child, value[index], emit, native_bytes)
257
+ for index, child in enumerate(codec.values)]
258
+ return True if emit else tuple(result)
259
+ if cls is types.Option:
260
+ start = len(self.data)
261
+ if emit:
262
+ self.data.append(0)
263
+ result = None if value is None else self.prepare(codec.some, value, emit, native_bytes)
264
+ if emit and result is not None:
265
+ self.data[start] = 1
266
+ elif emit:
267
+ # Option(Null) and nested Options normalize to None too.
268
+ del self.data[start + 1:]
269
+ return result
270
+ if cls is types.Enum:
271
+ if type(value) not in (dict, str):
272
+ fields = self.fields(value)
273
+ if codec._deserialize_type and isinstance(value, codec._deserialize_type):
274
+ value = {name: item for name, item in fields.items() if item is not None}
275
+ else:
276
+ raise _UseWrappers
277
+ if type(value) is str:
278
+ value = {value: None}
279
+ if len(value) != 1:
280
+ raise _UseWrappers
281
+ name, item = next(iter(value.items()))
282
+ if type(name) is not str:
283
+ raise _UseWrappers
284
+ for index, (variant, child) in enumerate(codec.variants.items()):
285
+ if type(child) is dict:
286
+ index, child = child.get('id', index), child.get('type', child)
287
+ if variant == name:
288
+ if type(index) is not int or not 0 <= index <= 255:
289
+ raise _UseWrappers
290
+ if emit:
291
+ self.data.append(index)
292
+ if child is None:
293
+ return name
294
+ item = self.prepare(child, item, emit, native_bytes)
295
+ return True if emit else {name: item}
296
+ raise _UseWrappers
297
+ if cls in (types.Vec, types.BoundedVec, types.Array):
298
+ array = cls is types.Array
299
+ if type(value) is bytearray:
300
+ value = bytes(value)
301
+ if type(value) not in (list, tuple, bytes):
302
+ raise _UseWrappers
303
+ if array and (type(value) is tuple or len(value) != codec.length):
304
+ raise _UseWrappers
305
+ if type(value) is list and not value:
306
+ if cls is types.BoundedVec:
307
+ codec._check_count(0)
308
+ if emit and not array:
309
+ self.data.append(0)
310
+ return True if emit else []
311
+ if not array and codec.type_def is types.U8 and type(value) is not bytes:
312
+ raise _UseWrappers
313
+ if cls is types.BoundedVec:
314
+ codec._check_count(len(value))
315
+ if codec.type_def is types.U8 and type(value) is bytes:
316
+ result = value if native_bytes else codec.serialize(value)
317
+ if emit:
318
+ if not array:
319
+ self.length(len(value))
320
+ self.data.extend(value)
321
+ return result
322
+ if type(value) not in (list, tuple) or (array and type(value) is not list):
323
+ raise _UseWrappers
324
+ child_class = self.check(codec.type_def)
325
+ if (emit and child_class is types.HashDef
326
+ and all(type(item) is bytes and len(item) == codec.type_def.byte_count for item in value)):
327
+ if not array:
328
+ self.length(len(value))
329
+ self.data.extend(b''.join(value))
330
+ return True
331
+ if emit and child_class in _INTEGERS and all(type(item) is int for item in value):
332
+ if child_class is types.UnsignedInteger:
333
+ if not array:
334
+ self.length(len(value))
335
+ for item in value:
336
+ self.data.extend(item.to_bytes(codec.type_def.byte_count, 'little'))
337
+ else:
338
+ self.data.extend(codec.encode(list(value) if type(value) is tuple else value).data)
339
+ return True
340
+ if emit and not array:
341
+ self.length(len(value))
342
+ result = [self.prepare(codec.type_def, item, emit, native_bytes) for item in value]
343
+ return True if emit else result
344
+ if type(value) not in (dict, list, tuple):
345
+ raise _UseWrappers
346
+ if not value:
347
+ if emit:
348
+ self.data.append(0)
349
+ return []
350
+ # Maps sort serialized representations, including values for duplicate
351
+ # keys. Keep that representation and let the existing encoder sort it.
352
+ items = value.items() if type(value) is dict else value
353
+ result = []
354
+ for pair in items:
355
+ if type(pair) not in (tuple, list) or len(pair) != 2:
356
+ raise _UseWrappers
357
+ key, item = pair
358
+ result.append((self.prepare(codec.key_def, key, False, False),
359
+ self.prepare(codec.value_def, item, False, False)))
360
+ if emit:
361
+ self.data.extend(codec.encode(result).data)
362
+ return result
363
+
364
+
365
+ def encode_from_object(codec, value):
366
+ """Return binary data for standard inputs, or None to use normal wrappers."""
367
+ # Bypassing wrappers pays off for collection models. Small mixed records
368
+ # are cheaper through the existing normalization path.
369
+ if (type(codec) is not types.Struct or type(codec.arguments) is not dict
370
+ or not codec.arguments or not all(type(child) in (
371
+ types.Array, types.Vec, types.BoundedVec, types.Map,
372
+ ) for child in codec.arguments.values())):
373
+ return None
374
+ encoder = _Encoder()
375
+ try:
376
+ encoder.prepare(codec, value)
377
+ except (_UseWrappers, ValueError, TypeError, ArithmeticError, IndexError,
378
+ KeyError, AttributeError, RecursionError, struct.error):
379
+ # Probing uses only built-in hooks and plain inputs. On invalid or
380
+ # unsupported data, repeat the original path so normalization errors
381
+ # still take precedence over encode-time errors in earlier fields.
382
+ return None
383
+ return JamBytes(encoder.data)
@@ -8,6 +8,7 @@ import json
8
8
  from jamcodec.base import JamCodecTypeDef, JamCodecType, JamBytes
9
9
  from jamcodec.types import Struct, StructObject, Option, Vec, Enum
10
10
  from jamcodec.decoding import decode_to_object
11
+ from jamcodec.encoding import encode_from_object
11
12
 
12
13
  T = TypeVar('T')
13
14
 
@@ -69,7 +70,17 @@ class Serializable:
69
70
  return scale_type.to_serializable_obj()
70
71
 
71
72
  def to_jam_bytes(self) -> JamBytes:
72
- scale_obj = self.to_codec_type()
73
+ to_codec_type = self.to_codec_type
74
+ if (getattr(to_codec_type, '__func__', None) is _DEFAULT_TO_CODEC_TYPE
75
+ and is_dataclass(self)):
76
+ codec = self.to_codec_def()
77
+ data = encode_from_object(codec, self)
78
+ if data is not None:
79
+ return data
80
+ scale_obj = codec.new()
81
+ scale_obj.deserialize(self)
82
+ else:
83
+ scale_obj = to_codec_type()
73
84
  if type(scale_obj) in (JamCodecType, StructObject):
74
85
  # The value is already normalized; only the encoded bytes are needed.
75
86
  return scale_obj.type_def.encode(scale_obj.value_serialized)
@@ -106,3 +117,6 @@ class Serializable:
106
117
  for field in dataclasses.fields(self):
107
118
  if getattr(self, field.name, None) is not None:
108
119
  return field.name, getattr(self, field.name)
120
+
121
+
122
+ _DEFAULT_TO_CODEC_TYPE = Serializable.to_codec_type
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jamcodec
3
- Version: 0.1.8
3
+ Version: 0.1.9
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
@@ -6,6 +6,7 @@ jamcodec/__init__.py
6
6
  jamcodec/base.py
7
7
  jamcodec/constants.py
8
8
  jamcodec/decoding.py
9
+ jamcodec/encoding.py
9
10
  jamcodec/exceptions.py
10
11
  jamcodec/mixins.py
11
12
  jamcodec/types.py
@@ -23,6 +24,7 @@ test/test_bytes.py
23
24
  test/test_collection_buffers.py
24
25
  test/test_compact.py
25
26
  test/test_direct_decode.py
27
+ test/test_direct_encode.py
26
28
  test/test_enum.py
27
29
  test/test_float.py
28
30
  test/test_hashmap.py
@@ -0,0 +1,440 @@
1
+ from dataclasses import dataclass, field
2
+ from types import MethodType
3
+ from unittest.mock import patch
4
+
5
+ import pytest
6
+
7
+ from jamcodec.base import JamBytes, JamCodecType
8
+ from jamcodec.encoding import encode_from_object
9
+ from jamcodec.exceptions import ScaleDeserializeException
10
+ from jamcodec.mixins import Serializable
11
+ from jamcodec.types import (
12
+ Array, BitArray, Bool, BoundedVec, Bytes, BytesDef, Enum, F32, F64, H256, I16, Map, Null,
13
+ Option, Struct, StructObject, Tuple, U8, U16, U32, UnsignedInteger, VarInt64, Vec,
14
+ )
15
+
16
+
17
+ def legacy_encode(value):
18
+ """The binary encoding path before direct normalization was introduced."""
19
+ wrapper = value.to_codec_type()
20
+ if type(wrapper) in (JamCodecType, StructObject):
21
+ return wrapper.type_def.encode(wrapper.value_serialized)
22
+ return wrapper.encode()
23
+
24
+
25
+ def assert_matches_legacy(value):
26
+ assert_encoding_matches_legacy(value)
27
+ # Exercise the collection path too when the original model is a mixed
28
+ # record, which deliberately keeps the normal wrapper path at the root.
29
+ assert_encoding_matches_legacy(model(Vec(value.to_codec_def()), [value]))
30
+
31
+
32
+ def assert_encoding_matches_legacy(value):
33
+ try:
34
+ expected = legacy_encode(value)
35
+ except Exception as error:
36
+ with pytest.raises(type(error)) as caught:
37
+ value.to_jam_bytes()
38
+ assert str(caught.value) == str(error)
39
+ else:
40
+ actual = value.to_jam_bytes()
41
+ assert actual.to_bytes() == expected.to_bytes()
42
+ assert actual.offset == 0
43
+ assert actual.length == expected.length
44
+
45
+
46
+ @dataclass
47
+ class Item(Serializable):
48
+ number: int = field(metadata={'codec': U16})
49
+ payload: bytes = field(metadata={'codec': Bytes})
50
+
51
+
52
+ @dataclass
53
+ class Choice(Serializable):
54
+ item: object = None
55
+ empty: object = None
56
+ _codec_type_def = Enum(item={'id': 17, 'type': Item.to_codec_def()}, empty={'id': 255, 'type': Null})
57
+
58
+
59
+ @dataclass
60
+ class Packet(Serializable):
61
+ records: list = field(metadata={'codec': BoundedVec(Item.to_codec_def(), 3, minimum=1)})
62
+ optional: object = field(metadata={'codec': Option(Item.to_codec_def())})
63
+ lookup: dict = field(metadata={'codec': Map(Tuple(H256, U16), Option(Bytes))})
64
+ pair: tuple = field(metadata={'codec': Tuple(U8, Array(U16, 2))})
65
+ fixed: bytes = field(metadata={'codec': Array(U8, 2)})
66
+ bits: list = field(metadata={'codec': BitArray(9)})
67
+ choice: Choice = field(metadata={'codec': Choice.to_codec_def()})
68
+
69
+
70
+ @pytest.mark.parametrize('choice', [Choice(item=Item(7, b'item')), Choice(empty=Null)])
71
+ def test_standard_nested_models_use_direct_normalization(choice):
72
+ value = Packet(
73
+ [Item(1, b'first'), Item(258, b'')], Item(65535, b'optional'),
74
+ {(bytes(32), 2): None, (b'\xff' * 32, 1): b'value'},
75
+ (3, [1, 258]), bytearray(b'ab'), [True] * 9, choice,
76
+ )
77
+ collection = model(Vec(Packet.to_codec_def()), [value])
78
+ assert encode_from_object(collection.to_codec_def(), collection) is not None
79
+ assert_matches_legacy(value)
80
+
81
+
82
+ def model(codec, value):
83
+ @dataclass
84
+ class Value(Serializable):
85
+ item: object = field(metadata={'codec': codec})
86
+
87
+ return Value(value)
88
+
89
+
90
+ @pytest.mark.parametrize('codec,value', [
91
+ (Array(U8, 2), [1, 255]),
92
+ (Array(U8, 2), 'é'),
93
+ (Array(U8, 2), '0x01ff'),
94
+ (Array(U16, 2), [1, 258]),
95
+ (Tuple(U16), 258),
96
+ (Tuple(U8, U16), [1, 258, 999]),
97
+ (Vec(U16), (1, 258)),
98
+ (Vec(U8), 'é'),
99
+ (BoundedVec(U8, 2, minimum=2, multiple=2), 'é'),
100
+ (Option(Null), Null),
101
+ (Option(Option(U16)), None),
102
+ (Bytes, [0, 255]),
103
+ (Bytes, '0x00ff'),
104
+ (H256, bytearray(range(32))),
105
+ (BitArray(9), [False] * 9),
106
+ ])
107
+ def test_accepted_input_representations_match(codec, value):
108
+ assert_matches_legacy(model(codec, value))
109
+
110
+
111
+ @pytest.mark.parametrize('codec,value', [
112
+ (U8, True), (U8, '1'), (U8, 256), (H256, b'short'), (H256, 7),
113
+ (Array(U8, 2), [256, 1]), (Array(U8, 2), [True, 1]),
114
+ (Array(U8, 2), b'x'), (Array(U16, 2), b'ab'), (Tuple(U8, U16), [1]),
115
+ (BoundedVec(U16, 4, minimum=2, multiple=2), [1, 2, 3]),
116
+ (BoundedVec(U8, 2, minimum=2), 'éa'), (Bytes, '0xgg'),
117
+ (BitArray(9), ['x']),
118
+ ])
119
+ def test_validation_errors_match(codec, value):
120
+ assert_matches_legacy(model(codec, value))
121
+
122
+
123
+ @pytest.mark.parametrize('first,second,first_codec,second_codec', [
124
+ (256, 7, U8, H256),
125
+ (256, '0xgg', U8, Bytes),
126
+ (b'short', 'not an integer', H256, U8),
127
+ ([256, 1], 'not an integer', Array(U8, 2), U8),
128
+ ])
129
+ def test_all_normalization_finishes_before_binary_validation(first, second, first_codec, second_codec):
130
+ @dataclass
131
+ class Invalid(Serializable):
132
+ first: object = field(metadata={'codec': first_codec})
133
+ second: object = field(metadata={'codec': second_codec})
134
+
135
+ assert_matches_legacy(Invalid(first, second))
136
+
137
+
138
+ @pytest.mark.parametrize('first,first_codec', [
139
+ ([256, 1], Vec(U8)),
140
+ ([256, 1], BoundedVec(U8, 1)),
141
+ (1e100, F32),
142
+ (-1, VarInt64),
143
+ ([(1, None), (1, b'x')], Map(U8, Option(Bytes))),
144
+ ])
145
+ def test_early_binary_errors_defer_to_later_normalization_errors(first, first_codec):
146
+ @dataclass
147
+ class Invalid(Serializable):
148
+ first: object = field(metadata={'codec': first_codec})
149
+ second: object = field(metadata={'codec': H256})
150
+
151
+ assert_matches_legacy(Invalid(first, 7))
152
+
153
+
154
+ @pytest.mark.parametrize('codec,value', [
155
+ (Vec(U8), b'abc'),
156
+ (Array(U8, 2), [1, 255]),
157
+ (Array(U16, 2), [1, 65535]),
158
+ (Array(H256, 2), [bytes(32), b'\xff' * 32]),
159
+ (BoundedVec(H256, 3), [bytes(32)]),
160
+ (Vec(I16), [-32768, 32767]),
161
+ (Vec(F32), [0.5, float('inf')]),
162
+ (Vec(F64), [0.5, float('nan')]),
163
+ (Tuple(Bool(), U32), (True, 2**32 - 1)),
164
+ (Option(Option(Null)), Null),
165
+ ])
166
+ def test_native_values_use_the_direct_path(codec, value):
167
+ value = model(codec, value)
168
+ value = model(Vec(value.to_codec_def()), [value])
169
+ assert encode_from_object(value.to_codec_def(), value) == legacy_encode(value)
170
+
171
+
172
+ @pytest.mark.parametrize('codec,value', [
173
+ (Map(Bytes, Bytes), [(b'key', b'z'), (b'key', b'a')]),
174
+ (Map(Option(Bytes), U8), [(None, 1), (b'x', 2)]),
175
+ (Map(U8, Option(H256)), [(1, None), (1, bytes(32))]),
176
+ (Map(Array(U8, 2), U8), [(bytearray(b'ab'), 1), (b'aa', 2)]),
177
+ (Map(U8, Enum(a=None, b=None)), [(1, 'b'), (1, 'a')]),
178
+ (Map(U8, U8), [(1, 2, 3)]),
179
+ ])
180
+ def test_map_sorting_uses_the_legacy_serialized_representations(codec, value):
181
+ assert_matches_legacy(model(codec, value))
182
+
183
+
184
+ def test_custom_codec_hooks_run_once_and_in_the_original_order():
185
+ events = []
186
+
187
+ class CustomByte(UnsignedInteger):
188
+ def deserialize(self, value):
189
+ events.append('deserialize')
190
+ return super().deserialize(value) + 1
191
+
192
+ def serialize(self, value):
193
+ events.append('serialize')
194
+ return value + 10
195
+
196
+ def encode(self, value):
197
+ events.append('encode')
198
+ return super().encode(value)
199
+
200
+ @dataclass
201
+ class Value(Serializable):
202
+ first: bytes = field(metadata={'codec': Bytes})
203
+ custom: int = field(metadata={'codec': CustomByte(8)})
204
+
205
+ value = Value(b'first', 2)
206
+ expected = legacy_encode(value)
207
+ events.clear()
208
+ assert value.to_jam_bytes() == expected
209
+ assert events == ['deserialize', 'serialize', 'encode']
210
+
211
+
212
+ def test_custom_root_wrapper_keeps_post_encode_validation():
213
+ events = []
214
+
215
+ class Wrapper(StructObject):
216
+ def deserialize(self, value):
217
+ events.append(('deserialize', self._data is not None))
218
+ return super().deserialize(value)
219
+
220
+ def encode(self, value=None):
221
+ events.append(('encode',))
222
+ return JamBytes(b'\xaa' + super().encode(value).to_bytes())
223
+
224
+ @dataclass
225
+ class Value(Serializable):
226
+ number: int
227
+ _codec_type_def = Struct(number=U8).impl(Wrapper)
228
+
229
+ expected = legacy_encode(Value(7))
230
+ events.clear()
231
+ assert Value(7).to_jam_bytes() == expected
232
+ assert events == [('deserialize', False), ('encode',), ('deserialize', True)]
233
+
234
+
235
+ def test_custom_to_codec_type_is_called_once():
236
+ calls = []
237
+
238
+ @dataclass
239
+ class Value(Serializable):
240
+ number: int = field(metadata={'codec': U8})
241
+
242
+ def to_codec_type(self):
243
+ calls.append('to_codec_type')
244
+ return super().to_codec_type()
245
+
246
+ assert Value(7).to_jam_bytes().to_bytes() == b'\x07'
247
+ assert calls == ['to_codec_type']
248
+
249
+
250
+ def test_replaced_base_to_codec_type_is_called_once():
251
+ original = Serializable.to_codec_type
252
+ calls = []
253
+
254
+ def custom(self):
255
+ calls.append('to_codec_type')
256
+ return original(self)
257
+
258
+ with patch.object(Serializable, 'to_codec_type', custom):
259
+ assert model(Vec(U16), [1, 258]).to_jam_bytes() == JamBytes(b'\x02\x01\x00\x02\x01')
260
+ assert calls == ['to_codec_type']
261
+
262
+
263
+ def test_mutable_definitions_and_wrapper_changes_are_not_cached_between_calls():
264
+ @dataclass
265
+ class Value(Serializable):
266
+ number: int
267
+ _codec_type_def = Struct(number=U8)
268
+
269
+ assert Value(7).to_jam_bytes().to_bytes() == b'\x07'
270
+ Value.to_codec_def().arguments['number'] = U16
271
+ assert Value(258).to_jam_bytes().to_bytes() == b'\x02\x01'
272
+
273
+ class Prefix(StructObject):
274
+ def encode(self, value=None):
275
+ return JamBytes(b'\xaa' + super().encode(value).to_bytes())
276
+
277
+ Value.to_codec_def().impl(Prefix)
278
+ assert Value(258).to_jam_bytes().to_bytes() == b'\xaa\x02\x01'
279
+
280
+
281
+ @pytest.mark.parametrize('hook', ['deserialize', 'serialize', 'encode'])
282
+ def test_builtin_instance_hooks_are_preserved(hook):
283
+ codec = UnsignedInteger(8)
284
+ value = model(codec, 2)
285
+ assert value.to_jam_bytes().to_bytes() == b'\x02'
286
+ calls = []
287
+ original = getattr(codec, hook)
288
+
289
+ def custom(self, item):
290
+ calls.append(hook)
291
+ return original(item)
292
+
293
+ setattr(codec, hook, MethodType(custom, codec))
294
+ expected = legacy_encode(value)
295
+ calls.clear()
296
+ assert value.to_jam_bytes() == expected
297
+ assert calls == [hook]
298
+
299
+
300
+ def test_class_serialization_hooks_preserve_validation():
301
+ original = UnsignedInteger.serialize
302
+
303
+ def checked(self, value):
304
+ if value == 255:
305
+ raise ScaleDeserializeException('class validation')
306
+ return original(self, value)
307
+
308
+ with patch.object(UnsignedInteger, 'serialize', checked):
309
+ assert_matches_legacy(model(Vec(U16), [1, 255]))
310
+
311
+
312
+ def test_byte_vectors_preserve_indirect_bytes_hooks():
313
+ def reject(self, value):
314
+ raise ScaleDeserializeException('indirect byte validation')
315
+
316
+ with patch.object(Bytes, 'serialize', MethodType(reject, Bytes)):
317
+ assert_matches_legacy(model(BoundedVec(U8, 4), b'ab'))
318
+
319
+
320
+ def test_custom_iterable_is_consumed_once():
321
+ for use_generator in (False, True):
322
+ calls = []
323
+
324
+ class Values(list):
325
+ def __iter__(self):
326
+ calls.append('iterate')
327
+ return super().__iter__()
328
+
329
+ def generate():
330
+ calls.append('iterate')
331
+ yield 1
332
+ yield 258
333
+
334
+ value = model(Vec(U16), generate() if use_generator else Values([1, 258]))
335
+ assert value.to_jam_bytes().to_bytes() == b'\x02\x01\x00\x02\x01'
336
+ assert calls == ['iterate']
337
+
338
+
339
+ def test_fallback_precedes_custom_attribute_access_and_schema_mutation():
340
+ def make():
341
+ calls = []
342
+ payload_codec = BytesDef()
343
+ encode = payload_codec.encode
344
+
345
+ @dataclass
346
+ class Nested(Serializable):
347
+ number: int = field(metadata={'codec': U8})
348
+
349
+ def read_number(self):
350
+ calls.append('get_number')
351
+
352
+ def require_hex(self, value):
353
+ assert type(value) is str
354
+ return encode(value)
355
+
356
+ payload_codec.encode = MethodType(require_hex, payload_codec)
357
+ return self.__dict__['number']
358
+
359
+ value = Nested(7)
360
+ Nested.number = property(read_number)
361
+
362
+ @dataclass
363
+ class Outer(Serializable):
364
+ payload: bytes = field(metadata={'codec': payload_codec})
365
+ nested: Nested = field(metadata={'codec': Nested.to_codec_def()})
366
+
367
+ return Outer(b'abc', value), calls
368
+
369
+ old, old_calls = make()
370
+ expected = legacy_encode(old)
371
+ new, new_calls = make()
372
+ assert new.to_jam_bytes() == expected
373
+ assert old_calls == new_calls == ['get_number']
374
+
375
+
376
+ def test_custom_truthiness_is_not_invoked_during_fast_path_probing():
377
+ calls = []
378
+
379
+ class Integer(int):
380
+ def __bool__(self):
381
+ calls.append('bool')
382
+ return True
383
+
384
+ value = model(U8, Integer(7))
385
+ with pytest.raises(ValueError, match='Value must be an integer'):
386
+ value.to_jam_bytes()
387
+ assert calls == ['bool']
388
+
389
+
390
+ def test_custom_truthiness_in_a_collection_is_invoked_once():
391
+ calls = []
392
+
393
+ class Integer(int):
394
+ def __bool__(self):
395
+ calls.append('bool')
396
+ return True
397
+
398
+ value = model(Vec(U16), [Integer(7)])
399
+ with pytest.raises(ValueError, match='Value must be an integer'):
400
+ value.to_jam_bytes()
401
+ assert calls == ['bool']
402
+
403
+
404
+ def test_direct_encoding_does_not_mutate_input_collections_or_buffers():
405
+ @dataclass
406
+ class Collection(Serializable):
407
+ numbers: list = field(metadata={'codec': Array(U16, 2)})
408
+ hashes: list = field(metadata={'codec': Vec(H256)})
409
+ lookup: dict = field(metadata={'codec': Map(Bytes, Bytes)})
410
+
411
+ numbers = [1, 258]
412
+ digest = bytearray(range(32))
413
+ hashes = [digest]
414
+ lookup = {b'z': b'last', b'a': b'first'}
415
+ value = Collection(numbers, hashes, lookup)
416
+ first = value.to_jam_bytes()
417
+ assert first == legacy_encode(value)
418
+ assert value.numbers is numbers and numbers == [1, 258]
419
+ assert value.hashes is hashes and hashes[0] is digest and digest == bytearray(range(32))
420
+ assert value.lookup is lookup and list(lookup) == [b'z', b'a']
421
+ numbers[0] = 2
422
+ digest[0] = 255
423
+ assert first != value.to_jam_bytes()
424
+
425
+
426
+ @pytest.mark.parametrize('kind', ['array', 'vec', 'bounded', 'map'])
427
+ def test_empty_collections_do_not_construct_elements(kind):
428
+ class NoElement(UnsignedInteger):
429
+ def new(self, **kwargs):
430
+ raise AssertionError('An empty collection must not construct elements')
431
+
432
+ child = NoElement(8)
433
+ codec = {
434
+ 'array': Array(child, 0),
435
+ 'vec': Vec(child),
436
+ 'bounded': BoundedVec(child, 0),
437
+ 'map': Map(child, child),
438
+ }[kind]
439
+ value = model(codec, {} if kind == 'map' else [])
440
+ assert encode_from_object(value.to_codec_def(), value) == legacy_encode(value)
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
File without changes