jamcodec 0.1.7__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.7 → jamcodec-0.1.9}/PKG-INFO +1 -1
  2. jamcodec-0.1.9/jamcodec/decoding.py +125 -0
  3. jamcodec-0.1.9/jamcodec/encoding.py +383 -0
  4. {jamcodec-0.1.7 → jamcodec-0.1.9}/jamcodec/mixins.py +20 -2
  5. {jamcodec-0.1.7 → jamcodec-0.1.9}/jamcodec.egg-info/PKG-INFO +1 -1
  6. {jamcodec-0.1.7 → jamcodec-0.1.9}/jamcodec.egg-info/SOURCES.txt +4 -0
  7. jamcodec-0.1.9/test/test_direct_decode.py +385 -0
  8. jamcodec-0.1.9/test/test_direct_encode.py +440 -0
  9. {jamcodec-0.1.7 → jamcodec-0.1.9}/LICENSE +0 -0
  10. {jamcodec-0.1.7 → jamcodec-0.1.9}/README.md +0 -0
  11. {jamcodec-0.1.7 → jamcodec-0.1.9}/jamcodec/__init__.py +0 -0
  12. {jamcodec-0.1.7 → jamcodec-0.1.9}/jamcodec/base.py +0 -0
  13. {jamcodec-0.1.7 → jamcodec-0.1.9}/jamcodec/constants.py +0 -0
  14. {jamcodec-0.1.7 → jamcodec-0.1.9}/jamcodec/exceptions.py +0 -0
  15. {jamcodec-0.1.7 → jamcodec-0.1.9}/jamcodec/types.py +0 -0
  16. {jamcodec-0.1.7 → jamcodec-0.1.9}/jamcodec.egg-info/dependency_links.txt +0 -0
  17. {jamcodec-0.1.7 → jamcodec-0.1.9}/jamcodec.egg-info/requires.txt +0 -0
  18. {jamcodec-0.1.7 → jamcodec-0.1.9}/jamcodec.egg-info/top_level.txt +0 -0
  19. {jamcodec-0.1.7 → jamcodec-0.1.9}/pyproject.toml +0 -0
  20. {jamcodec-0.1.7 → jamcodec-0.1.9}/requirements.txt +0 -0
  21. {jamcodec-0.1.7 → jamcodec-0.1.9}/setup.cfg +0 -0
  22. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_array.py +0 -0
  23. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_bitarray.py +0 -0
  24. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_bitvec.py +0 -0
  25. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_boolean.py +0 -0
  26. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_bounded_vec.py +0 -0
  27. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_bytes.py +0 -0
  28. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_collection_buffers.py +0 -0
  29. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_compact.py +0 -0
  30. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_enum.py +0 -0
  31. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_float.py +0 -0
  32. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_hashmap.py +0 -0
  33. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_integer.py +0 -0
  34. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_map.py +0 -0
  35. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_mixins.py +0 -0
  36. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_option.py +0 -0
  37. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_scale_types.py +0 -0
  38. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_scalebytes.py +0 -0
  39. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_string.py +0 -0
  40. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_struct.py +0 -0
  41. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_tuple.py +0 -0
  42. {jamcodec-0.1.7 → jamcodec-0.1.9}/test/test_varint.py +0 -0
  43. {jamcodec-0.1.7 → 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.7
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,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()
@@ -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)
@@ -7,6 +7,8 @@ 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
11
+ from jamcodec.encoding import encode_from_object
10
12
 
11
13
  T = TypeVar('T')
12
14
 
@@ -68,7 +70,17 @@ class Serializable:
68
70
  return scale_type.to_serializable_obj()
69
71
 
70
72
  def to_jam_bytes(self) -> JamBytes:
71
- 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()
72
84
  if type(scale_obj) in (JamCodecType, StructObject):
73
85
  # The value is already normalized; only the encoded bytes are needed.
74
86
  return scale_obj.type_def.encode(scale_obj.value_serialized)
@@ -77,7 +89,10 @@ class Serializable:
77
89
 
78
90
  @classmethod
79
91
  def from_jam_bytes(cls: Type[T], scale_bytes: JamBytes) -> T:
80
- scale_obj = cls.to_codec_def().new()
92
+ codec = cls.to_codec_def()
93
+ if getattr(cls.from_codec_type, '__func__', None) is Serializable.from_codec_type.__func__:
94
+ return decode_to_object(codec, scale_bytes)
95
+ scale_obj = codec.new()
81
96
  scale_obj.decode(scale_bytes)
82
97
  return cls.from_codec_type(scale_obj)
83
98
 
@@ -102,3 +117,6 @@ class Serializable:
102
117
  for field in dataclasses.fields(self):
103
118
  if getattr(self, field.name, None) is not None:
104
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.7
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
@@ -5,6 +5,8 @@ requirements.txt
5
5
  jamcodec/__init__.py
6
6
  jamcodec/base.py
7
7
  jamcodec/constants.py
8
+ jamcodec/decoding.py
9
+ jamcodec/encoding.py
8
10
  jamcodec/exceptions.py
9
11
  jamcodec/mixins.py
10
12
  jamcodec/types.py
@@ -21,6 +23,8 @@ test/test_bounded_vec.py
21
23
  test/test_bytes.py
22
24
  test/test_collection_buffers.py
23
25
  test/test_compact.py
26
+ test/test_direct_decode.py
27
+ test/test_direct_encode.py
24
28
  test/test_enum.py
25
29
  test/test_float.py
26
30
  test/test_hashmap.py