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