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.
- {jamcodec-0.1.8 → jamcodec-0.1.9}/PKG-INFO +1 -1
- jamcodec-0.1.9/jamcodec/encoding.py +383 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/mixins.py +15 -1
- {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec.egg-info/PKG-INFO +1 -1
- {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec.egg-info/SOURCES.txt +2 -0
- jamcodec-0.1.9/test/test_direct_encode.py +440 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/LICENSE +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/README.md +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/__init__.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/base.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/constants.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/decoding.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/exceptions.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec/types.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec.egg-info/dependency_links.txt +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec.egg-info/requires.txt +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/jamcodec.egg-info/top_level.txt +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/pyproject.toml +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/requirements.txt +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/setup.cfg +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_array.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_bitarray.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_bitvec.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_boolean.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_bounded_vec.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_bytes.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_collection_buffers.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_compact.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_direct_decode.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_enum.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_float.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_hashmap.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_integer.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_map.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_mixins.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_option.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_scale_types.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_scalebytes.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_string.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_struct.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_tuple.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_varint.py +0 -0
- {jamcodec-0.1.8 → jamcodec-0.1.9}/test/test_vec.py +0 -0
|
@@ -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
|
-
|
|
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
|
|
@@ -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
|
|
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
|