soia-client 1.0.7__tar.gz → 1.0.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of soia-client might be problematic. Click here for more details.
- {soia_client-1.0.7 → soia_client-1.0.8}/PKG-INFO +1 -1
- {soia_client-1.0.7 → soia_client-1.0.8}/pyproject.toml +1 -1
- {soia_client-1.0.7 → soia_client-1.0.8}/soia_client.egg-info/PKG-INFO +1 -1
- {soia_client-1.0.7 → soia_client-1.0.8}/soia_client.egg-info/SOURCES.txt +0 -1
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/impl/arrays.py +1 -1
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/impl/enums.py +78 -35
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/impl/function_maker.py +1 -5
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/impl/optionals.py +0 -1
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/impl/primitives.py +17 -4
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/impl/repr.py +0 -1
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/impl/structs.py +68 -30
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/impl/type_adapter.py +0 -1
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/module_initializer.py +0 -1
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/serializer.py +7 -4
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/timestamp.py +1 -1
- {soia_client-1.0.7 → soia_client-1.0.8}/tests/test_module_initializer.py +295 -25
- {soia_client-1.0.7 → soia_client-1.0.8}/tests/test_serializers.py +32 -3
- soia_client-1.0.7/soialib/impl/encoding.py +0 -41
- {soia_client-1.0.7 → soia_client-1.0.8}/LICENSE +0 -0
- {soia_client-1.0.7 → soia_client-1.0.8}/README +0 -0
- {soia_client-1.0.7 → soia_client-1.0.8}/setup.cfg +0 -0
- {soia_client-1.0.7 → soia_client-1.0.8}/soia_client.egg-info/dependency_links.txt +0 -0
- {soia_client-1.0.7 → soia_client-1.0.8}/soia_client.egg-info/top_level.txt +0 -0
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/__init__.py +0 -0
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/impl/__init__.py +0 -0
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/keyed_items.py +0 -0
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/method.py +0 -0
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/never.py +0 -0
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/serializers.py +0 -0
- {soia_client-1.0.7 → soia_client-1.0.8}/soialib/spec.py +0 -0
- {soia_client-1.0.7 → soia_client-1.0.8}/tests/test_timestamp.py +0 -0
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
# TODO: unrecognized fields (and handles removed fields)
|
|
3
|
-
|
|
1
|
+
import copy
|
|
4
2
|
from collections.abc import Callable, Sequence
|
|
5
3
|
from dataclasses import FrozenInstanceError, dataclass
|
|
6
4
|
from typing import Any, Final, Union
|
|
7
5
|
|
|
8
6
|
from soialib import spec as _spec
|
|
9
|
-
from soialib.impl import
|
|
10
|
-
from soialib.impl.function_maker import (
|
|
11
|
-
BodyBuilder,
|
|
12
|
-
BodySpan,
|
|
13
|
-
Expr,
|
|
14
|
-
ExprLike,
|
|
15
|
-
Line,
|
|
16
|
-
make_function,
|
|
17
|
-
)
|
|
7
|
+
from soialib.impl.function_maker import BodyBuilder, Expr, ExprLike, Line, make_function
|
|
18
8
|
from soialib.impl.repr import repr_impl
|
|
19
9
|
from soialib.impl.type_adapter import TypeAdapter
|
|
20
10
|
|
|
@@ -40,7 +30,6 @@ class EnumAdapter(TypeAdapter):
|
|
|
40
30
|
|
|
41
31
|
private_is_enum_attr = _name_private_is_enum_attr(spec.id)
|
|
42
32
|
self.private_is_enum_attr = private_is_enum_attr
|
|
43
|
-
# TODO: comment
|
|
44
33
|
setattr(base_class, private_is_enum_attr, True)
|
|
45
34
|
|
|
46
35
|
# Add the constants.
|
|
@@ -81,7 +70,10 @@ class EnumAdapter(TypeAdapter):
|
|
|
81
70
|
setattr(base_class, f"wrap_{value_field.spec.name}", wrap_fn)
|
|
82
71
|
|
|
83
72
|
base_class._fj = _make_from_json_fn(
|
|
84
|
-
self.all_constant_fields,
|
|
73
|
+
self.all_constant_fields,
|
|
74
|
+
value_fields,
|
|
75
|
+
set(self.spec.removed_numbers),
|
|
76
|
+
base_class,
|
|
85
77
|
)
|
|
86
78
|
|
|
87
79
|
# Mark finalization as done.
|
|
@@ -116,7 +108,6 @@ class EnumAdapter(TypeAdapter):
|
|
|
116
108
|
|
|
117
109
|
def from_json_expr(self, json_expr: ExprLike) -> Expr:
|
|
118
110
|
fn_name = "_fj"
|
|
119
|
-
# TODO: comment
|
|
120
111
|
from_json_fn = getattr(self.gen_class, fn_name, None)
|
|
121
112
|
if from_json_fn:
|
|
122
113
|
return Expr.join(Expr.local("_fj?", from_json_fn), "(", json_expr, ")")
|
|
@@ -149,7 +140,6 @@ def _make_base_class(spec: _spec.Enum) -> type:
|
|
|
149
140
|
raise FrozenInstanceError(self.__class__.__qualname__)
|
|
150
141
|
|
|
151
142
|
def __eq__(self, other: Any) -> bool:
|
|
152
|
-
# TODO: make it work with unrecognized versus UNKNOWN
|
|
153
143
|
if isinstance(other, BaseClass):
|
|
154
144
|
return other.kind == self.kind and other.value == self.value
|
|
155
145
|
return NotImplemented
|
|
@@ -164,7 +154,7 @@ def _make_base_class(spec: _spec.Enum) -> type:
|
|
|
164
154
|
|
|
165
155
|
|
|
166
156
|
def _make_constant_class(base_class: type, spec: _spec.ConstantField) -> type:
|
|
167
|
-
class
|
|
157
|
+
class Constant(base_class):
|
|
168
158
|
__slots__ = ()
|
|
169
159
|
|
|
170
160
|
kind: Final[str] = spec.name
|
|
@@ -183,7 +173,37 @@ def _make_constant_class(base_class: type, spec: _spec.ConstantField) -> type:
|
|
|
183
173
|
def __repr__(self) -> str:
|
|
184
174
|
return f"{base_class.__qualname__}.{spec.attribute}"
|
|
185
175
|
|
|
186
|
-
return
|
|
176
|
+
return Constant
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _make_unrecognized_class(base_class: type) -> type:
|
|
180
|
+
"""Wraps around an unrecognized dense JSON.
|
|
181
|
+
|
|
182
|
+
Looks and acts just like the UNKNOWN constant, except that its JSON representation
|
|
183
|
+
is the original unrecognized dense JSON.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
class Unrecognized(base_class):
|
|
187
|
+
__slots__ = ("_dj",)
|
|
188
|
+
|
|
189
|
+
kind: Final[str] = "?"
|
|
190
|
+
_number: Final[int] = 0
|
|
191
|
+
# dense JSON
|
|
192
|
+
_dj: Any
|
|
193
|
+
# readable JSON
|
|
194
|
+
_rj: Final[str] = "?"
|
|
195
|
+
# has value
|
|
196
|
+
_hv: Final[bool] = False
|
|
197
|
+
|
|
198
|
+
def __init__(self, dj: Any):
|
|
199
|
+
# Do not call super().__init__().
|
|
200
|
+
object.__setattr__(self, "_dj", copy.deepcopy(dj))
|
|
201
|
+
object.__setattr__(self, "value", None)
|
|
202
|
+
|
|
203
|
+
def __repr__(self) -> str:
|
|
204
|
+
return f"{base_class.__qualname__}.UNKNOWN"
|
|
205
|
+
|
|
206
|
+
return Unrecognized
|
|
187
207
|
|
|
188
208
|
|
|
189
209
|
def _make_value_class(
|
|
@@ -193,7 +213,7 @@ def _make_value_class(
|
|
|
193
213
|
) -> type:
|
|
194
214
|
number = field_spec.number
|
|
195
215
|
|
|
196
|
-
class
|
|
216
|
+
class Value(base_class):
|
|
197
217
|
__slots__ = ()
|
|
198
218
|
|
|
199
219
|
kind: Final[str] = field_spec.name
|
|
@@ -213,7 +233,7 @@ def _make_value_class(
|
|
|
213
233
|
body = value_repr.repr
|
|
214
234
|
return f"{base_class.__qualname__}.wrap_{field_spec.name}({body})"
|
|
215
235
|
|
|
216
|
-
ret =
|
|
236
|
+
ret = Value
|
|
217
237
|
|
|
218
238
|
ret._dj = property(
|
|
219
239
|
make_function(
|
|
@@ -286,9 +306,13 @@ def _make_wrap_fn(field: _ValueField) -> Callable[[Any], Any]:
|
|
|
286
306
|
def _make_from_json_fn(
|
|
287
307
|
constant_fields: Sequence[_spec.ConstantField],
|
|
288
308
|
value_fields: Sequence[_ValueField],
|
|
309
|
+
removed_numbers: set[int],
|
|
289
310
|
base_class: type,
|
|
290
311
|
) -> Callable[[Any], Any]:
|
|
312
|
+
unrecognized_class = _make_unrecognized_class(base_class)
|
|
313
|
+
unrecognized_class_local = Expr.local("Unrecognized", unrecognized_class)
|
|
291
314
|
obj_setattr_local = Expr.local("obj_settatr", object.__setattr__)
|
|
315
|
+
removed_numbers_local = Expr.local("removed_numbers", removed_numbers)
|
|
292
316
|
|
|
293
317
|
key_to_constant: dict[Union[int, str], Any] = {}
|
|
294
318
|
for field in constant_fields:
|
|
@@ -318,12 +342,15 @@ def _make_from_json_fn(
|
|
|
318
342
|
builder.append_ln(" if json == 0:")
|
|
319
343
|
builder.append_ln(" return ", unknown_constant_local)
|
|
320
344
|
else:
|
|
345
|
+
# `json.__class__ is int` is significantly faster than `isinstance(json, int)`
|
|
321
346
|
builder.append_ln(" if json.__class__ is int:")
|
|
322
347
|
builder.append_ln(" try:")
|
|
323
348
|
builder.append_ln(" return ", key_to_constant_local, "[json]")
|
|
324
349
|
builder.append_ln(" except:")
|
|
325
|
-
|
|
326
|
-
|
|
350
|
+
if removed_numbers:
|
|
351
|
+
builder.append_ln(" if json in ", removed_numbers_local, ":")
|
|
352
|
+
builder.append_ln(" return ", unknown_constant_local)
|
|
353
|
+
builder.append_ln(" return ", unrecognized_class_local, "(json)")
|
|
327
354
|
|
|
328
355
|
def append_number_branches(numbers: list[int], indent: str) -> None:
|
|
329
356
|
if len(numbers) == 1:
|
|
@@ -332,7 +359,9 @@ def _make_from_json_fn(
|
|
|
332
359
|
value_class_local = Expr.local("cls?", field.value_class)
|
|
333
360
|
value_expr = field.field_type.from_json_expr(Expr.join("json[1]"))
|
|
334
361
|
builder.append_ln(f"{indent}ret = ", value_class_local, "()")
|
|
335
|
-
builder.append_ln(
|
|
362
|
+
builder.append_ln(
|
|
363
|
+
indent, obj_setattr_local, '(ret, "value", ', value_expr, ")"
|
|
364
|
+
)
|
|
336
365
|
builder.append_ln(f"{indent}return ret")
|
|
337
366
|
else:
|
|
338
367
|
indented = f" {indent}"
|
|
@@ -344,18 +373,24 @@ def _make_from_json_fn(
|
|
|
344
373
|
builder.append_ln(f"{indent}else:")
|
|
345
374
|
append_number_branches(numbers[mid_index:], indented)
|
|
346
375
|
|
|
376
|
+
# `json.__class__ is list` is significantly faster than `isinstance(json, list)`
|
|
347
377
|
builder.append_ln(" elif json.__class__ is list:")
|
|
378
|
+
builder.append_ln(" number = json[0]")
|
|
348
379
|
if not value_fields:
|
|
349
|
-
#
|
|
350
|
-
|
|
380
|
+
# The field was either removed or is an unrecognized field.
|
|
381
|
+
if removed_numbers:
|
|
382
|
+
builder.append_ln(" if number in ", removed_numbers_local, ":")
|
|
383
|
+
builder.append_ln(" return ", unknown_constant_local)
|
|
384
|
+
builder.append_ln(" return ", unrecognized_class_local, "(json)")
|
|
351
385
|
else:
|
|
352
386
|
if len(value_fields) == 1:
|
|
353
387
|
builder.append_ln(f" if number != {value_fields[0].spec.number}:")
|
|
354
388
|
else:
|
|
355
389
|
builder.append_ln(" if number not in ", value_keys_local, ":")
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
390
|
+
if removed_numbers:
|
|
391
|
+
builder.append_ln(" if number in ", removed_numbers_local, ":")
|
|
392
|
+
builder.append_ln(" return ", unknown_constant_local)
|
|
393
|
+
builder.append_ln(" return ", unrecognized_class_local, "(json)")
|
|
359
394
|
append_number_branches(sorted(numbers), " ")
|
|
360
395
|
|
|
361
396
|
# READABLE FORMAT
|
|
@@ -367,7 +402,7 @@ def _make_from_json_fn(
|
|
|
367
402
|
builder.append_ln(" try:")
|
|
368
403
|
builder.append_ln(" return ", key_to_constant_local, "[json]")
|
|
369
404
|
builder.append_ln(" except:")
|
|
370
|
-
#
|
|
405
|
+
# In readable mode, drop unrecognized values and use UNKNOWN instead.
|
|
371
406
|
builder.append_ln(" return ", unknown_constant_local)
|
|
372
407
|
|
|
373
408
|
def append_name_branches(names: list[str], indent: str) -> None:
|
|
@@ -377,30 +412,38 @@ def _make_from_json_fn(
|
|
|
377
412
|
value_class_local = Expr.local("cls?", field.value_class)
|
|
378
413
|
value_expr = field.field_type.from_json_expr("json['value']")
|
|
379
414
|
builder.append_ln(f"{indent}ret = ", value_class_local, "()")
|
|
380
|
-
builder.append_ln(
|
|
415
|
+
builder.append_ln(
|
|
416
|
+
indent, obj_setattr_local, '(ret, "value", ', value_expr, ")"
|
|
417
|
+
)
|
|
381
418
|
builder.append_ln(f"{indent}return ret")
|
|
382
419
|
else:
|
|
383
420
|
indented = f" {indent}"
|
|
384
421
|
mid_index = int(len(names) / 2)
|
|
385
422
|
mid_name = names[mid_index - 1]
|
|
386
423
|
operator = "==" if mid_index == 1 else "<="
|
|
387
|
-
builder.append_ln(f"{indent}if
|
|
424
|
+
builder.append_ln(f"{indent}if kind {operator} '{mid_name}':")
|
|
388
425
|
append_name_branches(names[0:mid_index], indented)
|
|
389
426
|
builder.append_ln(f"{indent}else:")
|
|
390
427
|
append_name_branches(names[mid_index:], indented)
|
|
391
428
|
|
|
392
429
|
builder.append_ln(" elif isinstance(json, dict):")
|
|
393
430
|
if not value_fields:
|
|
394
|
-
# TODO: comment
|
|
395
431
|
builder.append_ln(" return ", unknown_constant_local)
|
|
396
432
|
else:
|
|
397
|
-
builder.append_ln("
|
|
398
|
-
builder.append_ln(" if
|
|
399
|
-
# TODO: comment
|
|
433
|
+
builder.append_ln(" kind = json['kind']")
|
|
434
|
+
builder.append_ln(" if kind not in ", value_keys_local, ":")
|
|
400
435
|
builder.append_ln(" return ", unknown_constant_local)
|
|
401
436
|
builder.append_ln(" else:")
|
|
402
437
|
append_name_branches(sorted(names), " ")
|
|
403
438
|
|
|
439
|
+
# In the unlikely event that json.loads() returns an instance of a subclass of int.
|
|
440
|
+
builder.append_ln(" elif isinstance(json, int):")
|
|
441
|
+
builder.append_ln(" json = int(json)")
|
|
442
|
+
builder.append_ln(" elif isinstance(json, list):")
|
|
443
|
+
builder.append_ln(" json = list(json)")
|
|
444
|
+
builder.append_ln(" else:")
|
|
445
|
+
builder.append_ln(" return TypeError()")
|
|
446
|
+
|
|
404
447
|
return make_function(
|
|
405
448
|
name="from_json",
|
|
406
449
|
params=["json"],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import itertools
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import Any, Callable,
|
|
3
|
+
from typing import Any, Callable, Iterable, Optional, Sequence, Union
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def make_function(
|
|
@@ -120,15 +120,11 @@ class LineSpan:
|
|
|
120
120
|
_EMPTY_LINE_SPAN = LineSpan(())
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
# TODO: comment
|
|
124
123
|
LineSpanLike = Union[str, LineSpan]
|
|
125
124
|
|
|
126
125
|
|
|
127
126
|
# A type alias to use when the line span is a Python expression.
|
|
128
127
|
Expr = LineSpan
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
# TODO: comment
|
|
132
128
|
ExprLike = LineSpanLike
|
|
133
129
|
|
|
134
130
|
|
|
@@ -33,9 +33,9 @@ class _BoolAdapter(AbstractPrimitiveAdapter):
|
|
|
33
33
|
readable: bool,
|
|
34
34
|
) -> ExprLike:
|
|
35
35
|
if readable:
|
|
36
|
-
return Expr.join("(
|
|
36
|
+
return Expr.join("(True if ", in_expr, " else False)")
|
|
37
37
|
else:
|
|
38
|
-
return in_expr
|
|
38
|
+
return Expr.join("(1 if ", in_expr, " else 0)")
|
|
39
39
|
|
|
40
40
|
def from_json_expr(self, json_expr: ExprLike) -> Expr:
|
|
41
41
|
return Expr.join("(True if ", json_expr, " else False)")
|
|
@@ -52,6 +52,8 @@ class _AbstractIntAdapter(AbstractPrimitiveAdapter):
|
|
|
52
52
|
return "0"
|
|
53
53
|
|
|
54
54
|
def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
|
|
55
|
+
round_expr = Expr.local("round", round)
|
|
56
|
+
# Must accept float inputs and turn them into ints.
|
|
55
57
|
return Expr.join("(", arg_expr, " | 0)")
|
|
56
58
|
|
|
57
59
|
def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> ExprLike:
|
|
@@ -61,7 +63,18 @@ class _AbstractIntAdapter(AbstractPrimitiveAdapter):
|
|
|
61
63
|
return in_expr
|
|
62
64
|
|
|
63
65
|
def from_json_expr(self, json_expr: ExprLike) -> Expr:
|
|
64
|
-
|
|
66
|
+
# Must accept float inputs and turn them into ints.
|
|
67
|
+
return Expr.join(
|
|
68
|
+
"(",
|
|
69
|
+
json_expr,
|
|
70
|
+
" if ",
|
|
71
|
+
json_expr,
|
|
72
|
+
".__class__ is (0).__class__ else ",
|
|
73
|
+
Expr.local("_round", round),
|
|
74
|
+
"(",
|
|
75
|
+
json_expr,
|
|
76
|
+
"))",
|
|
77
|
+
)
|
|
65
78
|
|
|
66
79
|
|
|
67
80
|
@dataclass(frozen=True)
|
|
@@ -203,7 +216,7 @@ class _BytesAdapter(AbstractPrimitiveAdapter):
|
|
|
203
216
|
|
|
204
217
|
def from_json_expr(self, json_expr: ExprLike) -> Expr:
|
|
205
218
|
return Expr.join(
|
|
206
|
-
Expr.local("fromhex", _BytesAdapter._fromhex_fn), "(", json_expr, ")
|
|
219
|
+
Expr.local("fromhex", _BytesAdapter._fromhex_fn), "(", json_expr, ' or "")'
|
|
207
220
|
)
|
|
208
221
|
|
|
209
222
|
|
|
@@ -31,7 +31,6 @@ def repr_impl(input: Any) -> ReprResult:
|
|
|
31
31
|
return ReprResult(f"[\n{body}]", complex=True)
|
|
32
32
|
elif isinstance(input, str):
|
|
33
33
|
if "\n" in input:
|
|
34
|
-
# TODO: comment
|
|
35
34
|
lines = input.split("\n")
|
|
36
35
|
body = "".join(f" {repr(line)},\n" for line in lines)
|
|
37
36
|
return ReprResult(f"'\\n'.join([\n{body}])", complex=True)
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
from collections.abc import Callable, Sequence
|
|
2
3
|
from dataclasses import FrozenInstanceError, dataclass
|
|
3
|
-
from typing import Any, Final, Union
|
|
4
|
+
from typing import Any, Final, Union, cast
|
|
4
5
|
|
|
5
6
|
from soialib import spec as _spec
|
|
6
|
-
from soialib.impl.encoding import ARRAY_WIRE, LEN_BYTES, SMALL_ARRAY_WIRES
|
|
7
7
|
from soialib.impl.function_maker import (
|
|
8
8
|
BodyBuilder,
|
|
9
|
-
BodySpan,
|
|
10
9
|
Expr,
|
|
11
10
|
ExprLike,
|
|
12
11
|
Line,
|
|
@@ -50,8 +49,8 @@ class StructAdapter(TypeAdapter):
|
|
|
50
49
|
self.private_is_frozen_attr = _name_private_is_frozen_attr(spec.id)
|
|
51
50
|
|
|
52
51
|
slots = tuple(f.attribute for f in self.spec.fields) + (
|
|
53
|
-
#
|
|
54
|
-
"
|
|
52
|
+
# Unrecognized fields encountered during deserialization.
|
|
53
|
+
"_unrecognized",
|
|
55
54
|
# Lowest number greater than the number of every field with a non-default
|
|
56
55
|
# value.
|
|
57
56
|
"_array_len",
|
|
@@ -59,8 +58,8 @@ class StructAdapter(TypeAdapter):
|
|
|
59
58
|
frozen_class = self.gen_class = _make_dataclass(slots)
|
|
60
59
|
frozen_class.__name__ = spec.class_name
|
|
61
60
|
frozen_class.__qualname__ = spec.class_qualname
|
|
62
|
-
frozen_class.__setattr__ = _Frozen.__setattr__
|
|
63
|
-
frozen_class.__delattr__ = _Frozen.__delattr__
|
|
61
|
+
frozen_class.__setattr__ = cast(Any, _Frozen.__setattr__)
|
|
62
|
+
frozen_class.__delattr__ = cast(Any, _Frozen.__delattr__)
|
|
64
63
|
# We haven't added an __init__ method to the frozen class yet, so frozen_class()
|
|
65
64
|
# returns an object with no attribute set. We'll set the attributes of DEFAULT
|
|
66
65
|
# at the finalization step.
|
|
@@ -98,7 +97,6 @@ class StructAdapter(TypeAdapter):
|
|
|
98
97
|
)
|
|
99
98
|
)
|
|
100
99
|
|
|
101
|
-
# TODO: do I even need this?
|
|
102
100
|
# Aim to have dependencies finalized *before* the dependent. It's not always
|
|
103
101
|
# possible, because there can be cyclic dependencies.
|
|
104
102
|
# The function returned by the do_x_fn() method of a dependency is marginally
|
|
@@ -125,16 +123,21 @@ class StructAdapter(TypeAdapter):
|
|
|
125
123
|
setattr(frozen_class, self.private_is_frozen_attr, True)
|
|
126
124
|
setattr(mutable_class, self.private_is_frozen_attr, False)
|
|
127
125
|
|
|
128
|
-
frozen_class.__init__ =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
frozen_class.__init__ = cast(
|
|
127
|
+
Any,
|
|
128
|
+
_make_frozen_class_init_fn(
|
|
129
|
+
fields,
|
|
130
|
+
frozen_class=frozen_class,
|
|
131
|
+
simple_class=simple_class,
|
|
132
|
+
),
|
|
132
133
|
)
|
|
133
|
-
mutable_class.__init__ = _make_mutable_class_init_fn(fields)
|
|
134
|
+
mutable_class.__init__ = cast(Any, _make_mutable_class_init_fn(fields))
|
|
134
135
|
|
|
135
136
|
frozen_class.__eq__ = _make_eq_fn(fields)
|
|
136
|
-
frozen_class.__hash__ = _make_hash_fn(fields, self.record_hash)
|
|
137
|
-
frozen_class.__repr__ = mutable_class.__repr__ =
|
|
137
|
+
frozen_class.__hash__ = cast(Any, _make_hash_fn(fields, self.record_hash))
|
|
138
|
+
frozen_class.__repr__ = mutable_class.__repr__ = cast(
|
|
139
|
+
Any, _make_repr_fn(fields)
|
|
140
|
+
)
|
|
138
141
|
|
|
139
142
|
frozen_class._tdj = _make_to_dense_json_fn(fields=fields)
|
|
140
143
|
frozen_class._trj = _make_to_readable_json_fn(fields=fields)
|
|
@@ -162,7 +165,8 @@ class StructAdapter(TypeAdapter):
|
|
|
162
165
|
return Expr.local("_d?", self.gen_class.DEFAULT)
|
|
163
166
|
|
|
164
167
|
def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
|
|
165
|
-
#
|
|
168
|
+
# The goal of referring to private_is_frozen_attr is to raise an error if the
|
|
169
|
+
# struct has the wrong type.
|
|
166
170
|
return Expr.join(
|
|
167
171
|
"(",
|
|
168
172
|
arg_expr,
|
|
@@ -185,7 +189,7 @@ class StructAdapter(TypeAdapter):
|
|
|
185
189
|
|
|
186
190
|
def from_json_expr(self, json_expr: ExprLike) -> Expr:
|
|
187
191
|
fn_name = "_fj"
|
|
188
|
-
#
|
|
192
|
+
# The _fj method may not have been added to the class yet.
|
|
189
193
|
from_json_fn = getattr(self.gen_class, fn_name, None)
|
|
190
194
|
if from_json_fn:
|
|
191
195
|
return Expr.join(Expr.local("_fj?", from_json_fn), "(", json_expr, ")")
|
|
@@ -231,6 +235,8 @@ def _make_frozen_class_init_fn(
|
|
|
231
235
|
|
|
232
236
|
# Set the params.
|
|
233
237
|
params: Params = ["_self"]
|
|
238
|
+
if fields:
|
|
239
|
+
params.append("*")
|
|
234
240
|
for field in fields:
|
|
235
241
|
params.append(
|
|
236
242
|
Param(
|
|
@@ -262,8 +268,8 @@ def _make_frozen_class_init_fn(
|
|
|
262
268
|
spans.append("0")
|
|
263
269
|
return Expr.join(*spans)
|
|
264
270
|
|
|
265
|
-
if len(fields) <
|
|
266
|
-
# If the class has less than
|
|
271
|
+
if len(fields) < 3:
|
|
272
|
+
# If the class has less than 3 fields, it is faster to assign every field with
|
|
267
273
|
# object.__setattr__().
|
|
268
274
|
for field in fields:
|
|
269
275
|
attribute = field.field.attribute
|
|
@@ -275,6 +281,8 @@ def _make_frozen_class_init_fn(
|
|
|
275
281
|
field.type.to_frozen_expr(attribute),
|
|
276
282
|
")",
|
|
277
283
|
)
|
|
284
|
+
# Set the _unrecognized field.
|
|
285
|
+
builder.append_ln(obj_setattr, '(_self, "_unrecognized", ())')
|
|
278
286
|
# Set array length.
|
|
279
287
|
builder.append_ln(
|
|
280
288
|
obj_setattr,
|
|
@@ -300,6 +308,8 @@ def _make_frozen_class_init_fn(
|
|
|
300
308
|
" = ",
|
|
301
309
|
field.type.to_frozen_expr(attribute),
|
|
302
310
|
)
|
|
311
|
+
# Set the _unrecognized field.
|
|
312
|
+
builder.append_ln("_self._unrecognized = ()")
|
|
303
313
|
# Set array length.
|
|
304
314
|
builder.append_ln(f"_self._array_len = ", array_len_expr())
|
|
305
315
|
# Change back the __class__.
|
|
@@ -333,6 +343,7 @@ def _make_mutable_class_init_fn(fields: Sequence[_Field]) -> Callable[[Any], Non
|
|
|
333
343
|
" = ",
|
|
334
344
|
attribute,
|
|
335
345
|
)
|
|
346
|
+
builder.append_ln("_self._unrecognized = ()")
|
|
336
347
|
return make_function(
|
|
337
348
|
name="__init__",
|
|
338
349
|
params=params,
|
|
@@ -358,7 +369,7 @@ def _make_to_mutable_fn(
|
|
|
358
369
|
for field in fields:
|
|
359
370
|
attribute = field.field.attribute
|
|
360
371
|
builder.append_ln("ret.", attribute, " = self.", attribute)
|
|
361
|
-
|
|
372
|
+
builder.append_ln("ret._unrecognized = self._unrecognized")
|
|
362
373
|
builder.append_ln(
|
|
363
374
|
"ret.__class__ = ",
|
|
364
375
|
Expr.local("mutable_class", mutable_class),
|
|
@@ -396,10 +407,18 @@ def _make_to_frozen_fn(
|
|
|
396
407
|
field.type.to_frozen_expr(f"self.{attribute}"),
|
|
397
408
|
)
|
|
398
409
|
|
|
399
|
-
#
|
|
410
|
+
# Set the _unrecognized field.
|
|
411
|
+
builder.append_ln("ret._unrecognized = self._unrecognized")
|
|
400
412
|
|
|
401
413
|
def array_len_expr() -> Expr:
|
|
402
414
|
spans: list[LineSpanLike] = []
|
|
415
|
+
spans.append(
|
|
416
|
+
LineSpan.join(
|
|
417
|
+
f"{_get_num_slots(fields)} + ",
|
|
418
|
+
Expr.local("_len", len),
|
|
419
|
+
"(ret._unrecognized) if ret._unrecognized else ",
|
|
420
|
+
)
|
|
421
|
+
)
|
|
403
422
|
# Fields are sorted by number.
|
|
404
423
|
for field in reversed(fields):
|
|
405
424
|
attr_expr = f"ret.{field.field.attribute}"
|
|
@@ -413,8 +432,10 @@ def _make_to_frozen_fn(
|
|
|
413
432
|
spans.append("0")
|
|
414
433
|
return Expr.join(*spans)
|
|
415
434
|
|
|
435
|
+
# Set the _unrecognized field.
|
|
436
|
+
builder.append_ln("ret._unrecognized = self._unrecognized")
|
|
416
437
|
# Set array length.
|
|
417
|
-
builder.append_ln(
|
|
438
|
+
builder.append_ln("ret._array_len = ", array_len_expr())
|
|
418
439
|
|
|
419
440
|
builder.append_ln(
|
|
420
441
|
"ret.__class__ = ",
|
|
@@ -461,7 +482,6 @@ def _make_hash_fn(
|
|
|
461
482
|
"""
|
|
462
483
|
|
|
463
484
|
components: list[ExprLike] = []
|
|
464
|
-
# TODO: comment
|
|
465
485
|
components.append(str(record_hash))
|
|
466
486
|
for field in fields:
|
|
467
487
|
components.append(f"self.{field.field.attribute}")
|
|
@@ -487,8 +507,7 @@ def _make_repr_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
|
|
|
487
507
|
repr_local = Expr.local("repr", repr_impl)
|
|
488
508
|
for field in fields:
|
|
489
509
|
attribute = field.field.attribute
|
|
490
|
-
#
|
|
491
|
-
# TODO: comment; we use try because maybe in mutable we have the wrong type and we still want to be able to repr it
|
|
510
|
+
# is_not_default_expr only works on a frozen expression.
|
|
492
511
|
to_frozen_expr = field.type.to_frozen_expr(f"(self.{attribute})")
|
|
493
512
|
is_not_default_expr = field.type.is_not_default_expr(
|
|
494
513
|
to_frozen_expr, to_frozen_expr
|
|
@@ -512,7 +531,7 @@ def _make_repr_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
|
|
|
512
531
|
|
|
513
532
|
def _make_to_dense_json_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
|
|
514
533
|
builder = BodyBuilder()
|
|
515
|
-
builder.append_ln(
|
|
534
|
+
builder.append_ln("l = self._array_len")
|
|
516
535
|
builder.append_ln("ret = [0] * l")
|
|
517
536
|
for field in fields:
|
|
518
537
|
builder.append_ln(f"if l <= {field.field.number}:")
|
|
@@ -524,7 +543,10 @@ def _make_to_dense_json_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
|
|
|
524
543
|
readable=False,
|
|
525
544
|
),
|
|
526
545
|
)
|
|
527
|
-
|
|
546
|
+
num_slots = _get_num_slots(fields)
|
|
547
|
+
builder.append_ln(f"if l <= {num_slots}:")
|
|
548
|
+
builder.append_ln(" return ret")
|
|
549
|
+
builder.append_ln(f"ret[{num_slots}:] = self._unrecognized")
|
|
528
550
|
builder.append_ln("return ret")
|
|
529
551
|
return make_function(
|
|
530
552
|
name="to_dense_json",
|
|
@@ -550,7 +572,6 @@ def _make_to_readable_json_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
|
|
|
550
572
|
readable=True,
|
|
551
573
|
),
|
|
552
574
|
)
|
|
553
|
-
# TODO: unknown fields
|
|
554
575
|
builder.append_ln("return ret")
|
|
555
576
|
return make_function(
|
|
556
577
|
name="to_readable_json",
|
|
@@ -592,10 +613,20 @@ def _make_from_json_fn(
|
|
|
592
613
|
f" if array_len <= {number} else ",
|
|
593
614
|
field.type.from_json_expr(item_expr),
|
|
594
615
|
)
|
|
616
|
+
num_slots = _get_num_slots(fields)
|
|
617
|
+
builder.append_ln(f" if array_len <= {num_slots}:")
|
|
618
|
+
builder.append_ln(" ret._unrecognized = ()")
|
|
595
619
|
builder.append_ln(
|
|
596
|
-
|
|
620
|
+
" ret._array_len = ",
|
|
597
621
|
_adjust_array_len_expr("array_len", removed_numbers),
|
|
598
622
|
)
|
|
623
|
+
builder.append_ln(" else:")
|
|
624
|
+
builder.append_ln(
|
|
625
|
+
f" ret._unrecognized = ",
|
|
626
|
+
Expr.local("deepcopy", copy.deepcopy),
|
|
627
|
+
f"(json[{num_slots}:])",
|
|
628
|
+
)
|
|
629
|
+
builder.append_ln(" ret._array_len = array_len")
|
|
599
630
|
|
|
600
631
|
builder.append_ln("else:")
|
|
601
632
|
builder.append_ln(" array_len = 0")
|
|
@@ -611,7 +642,9 @@ def _make_from_json_fn(
|
|
|
611
642
|
)
|
|
612
643
|
builder.append_ln(" else:")
|
|
613
644
|
builder.append_ln(f" {lvalue} = ", field.type.default_expr())
|
|
614
|
-
|
|
645
|
+
# Drop unrecognized fields in readable mode.
|
|
646
|
+
builder.append_ln(" ret._unrecognized = ()")
|
|
647
|
+
builder.append_ln(" ret._array_len = array_len")
|
|
615
648
|
|
|
616
649
|
builder.append_ln("ret.__class__ = ", Expr.local("Frozen", frozen_class))
|
|
617
650
|
builder.append_ln("return ret")
|
|
@@ -683,6 +716,7 @@ def _init_default(default: Any, fields: Sequence[_Field]) -> None:
|
|
|
683
716
|
body=(Line.join("return ", field.type.default_expr()),),
|
|
684
717
|
)
|
|
685
718
|
object.__setattr__(default, attribute, get_field_default())
|
|
719
|
+
object.__setattr__(default, "_unrecognized", ())
|
|
686
720
|
object.__setattr__(default, "_array_len", 0)
|
|
687
721
|
|
|
688
722
|
|
|
@@ -728,3 +762,7 @@ def _name_private_is_frozen_attr(record_id: str) -> str:
|
|
|
728
762
|
record_name = _spec.RecordId.parse(record_id).name
|
|
729
763
|
hex_hash = hex(abs(hash(record_id)))[:6]
|
|
730
764
|
return f"_is_{record_name}_{hex_hash}"
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _get_num_slots(fields: Sequence[_Field]) -> int:
|
|
768
|
+
return (fields[-1].field.number + 1) if fields else 0
|
|
@@ -39,14 +39,17 @@ class Serializer(Generic[T]):
|
|
|
39
39
|
)
|
|
40
40
|
object.__setattr__(self, "_from_json_fn", _make_from_json_fn(adapter))
|
|
41
41
|
|
|
42
|
-
def to_json(self, input: T,
|
|
43
|
-
if
|
|
42
|
+
def to_json(self, input: T, readable=False) -> Any:
|
|
43
|
+
if readable:
|
|
44
44
|
return self._to_readable_json_fn(input)
|
|
45
45
|
else:
|
|
46
46
|
return self._to_dense_json_fn(input)
|
|
47
47
|
|
|
48
|
-
def to_json_code(self, input: T,
|
|
49
|
-
|
|
48
|
+
def to_json_code(self, input: T, readable=False) -> str:
|
|
49
|
+
if readable:
|
|
50
|
+
return jsonlib.dumps(self._to_readable_json_fn(input), indent=2)
|
|
51
|
+
else:
|
|
52
|
+
return jsonlib.dumps(self._to_dense_json_fn(input), separators=(",", ":"))
|
|
50
53
|
|
|
51
54
|
def from_json(self, json: Any) -> T:
|
|
52
55
|
return self._from_json_fn(json)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import dataclasses
|
|
3
2
|
import unittest
|
|
4
3
|
from typing import Any
|
|
5
4
|
|
|
@@ -25,10 +24,11 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
25
24
|
),
|
|
26
25
|
spec.Field(
|
|
27
26
|
name="y",
|
|
28
|
-
number=
|
|
27
|
+
number=2,
|
|
29
28
|
type=spec.PrimitiveType.FLOAT32,
|
|
30
29
|
),
|
|
31
30
|
),
|
|
31
|
+
removed_numbers=(1,),
|
|
32
32
|
),
|
|
33
33
|
spec.Struct(
|
|
34
34
|
id="my/module.soia:Segment",
|
|
@@ -115,6 +115,17 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
115
115
|
),
|
|
116
116
|
),
|
|
117
117
|
),
|
|
118
|
+
spec.Struct(
|
|
119
|
+
id="my/module.soia:After",
|
|
120
|
+
fields=(
|
|
121
|
+
spec.Field(
|
|
122
|
+
name="points",
|
|
123
|
+
number=0,
|
|
124
|
+
type=spec.ArrayType("my/module.soia:Point"),
|
|
125
|
+
has_mutable_getter=True,
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
),
|
|
118
129
|
spec.Enum(
|
|
119
130
|
id="my/module.soia:PrimaryColor",
|
|
120
131
|
constant_fields=(
|
|
@@ -137,7 +148,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
137
148
|
constant_fields=(
|
|
138
149
|
spec.ConstantField(
|
|
139
150
|
name="OK",
|
|
140
|
-
number=
|
|
151
|
+
number=1,
|
|
141
152
|
),
|
|
142
153
|
),
|
|
143
154
|
value_fields=(
|
|
@@ -154,36 +165,40 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
154
165
|
constant_fields=(
|
|
155
166
|
spec.ConstantField(
|
|
156
167
|
name="NULL",
|
|
157
|
-
number=
|
|
168
|
+
number=1,
|
|
158
169
|
),
|
|
159
170
|
),
|
|
160
171
|
value_fields=(
|
|
161
172
|
spec.ValueField(
|
|
162
173
|
name="bool",
|
|
163
|
-
number=
|
|
174
|
+
number=2,
|
|
164
175
|
type=spec.PrimitiveType.BOOL,
|
|
165
176
|
),
|
|
166
177
|
spec.ValueField(
|
|
167
178
|
name="number",
|
|
168
|
-
number=
|
|
179
|
+
number=3,
|
|
169
180
|
type=spec.PrimitiveType.FLOAT64,
|
|
170
181
|
),
|
|
171
182
|
spec.ValueField(
|
|
172
183
|
name="string",
|
|
173
|
-
number=
|
|
184
|
+
number=4,
|
|
174
185
|
type=spec.PrimitiveType.STRING,
|
|
175
186
|
),
|
|
176
187
|
spec.ValueField(
|
|
177
188
|
name="array",
|
|
178
|
-
number=
|
|
189
|
+
number=5,
|
|
179
190
|
type=spec.ArrayType("my/module.soia:JsonValue"),
|
|
180
191
|
),
|
|
181
192
|
spec.ValueField(
|
|
182
193
|
name="object",
|
|
183
|
-
number=
|
|
194
|
+
number=6,
|
|
184
195
|
type="my/module.soia:JsonValue.Object",
|
|
185
196
|
),
|
|
186
197
|
),
|
|
198
|
+
removed_numbers=(
|
|
199
|
+
100,
|
|
200
|
+
101,
|
|
201
|
+
),
|
|
187
202
|
),
|
|
188
203
|
spec.Struct(
|
|
189
204
|
id="my/module.soia:JsonValue.Object",
|
|
@@ -279,6 +294,27 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
279
294
|
),
|
|
280
295
|
),
|
|
281
296
|
),
|
|
297
|
+
spec.Struct(
|
|
298
|
+
id="my/module.soia:Foobar",
|
|
299
|
+
fields=(
|
|
300
|
+
spec.Field(
|
|
301
|
+
name="a",
|
|
302
|
+
number=1,
|
|
303
|
+
type=spec.PrimitiveType.INT32,
|
|
304
|
+
),
|
|
305
|
+
spec.Field(
|
|
306
|
+
name="b",
|
|
307
|
+
number=3,
|
|
308
|
+
type=spec.PrimitiveType.INT32,
|
|
309
|
+
),
|
|
310
|
+
spec.Field(
|
|
311
|
+
name="point",
|
|
312
|
+
number=4,
|
|
313
|
+
type="my/module.soia:Point",
|
|
314
|
+
),
|
|
315
|
+
),
|
|
316
|
+
removed_numbers=(0, 2),
|
|
317
|
+
),
|
|
282
318
|
),
|
|
283
319
|
methods=(
|
|
284
320
|
spec.Method(
|
|
@@ -299,7 +335,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
299
335
|
spec.Constant(
|
|
300
336
|
name="C",
|
|
301
337
|
type="my/module.soia:Point",
|
|
302
|
-
json_code="[1.5, 2.5]",
|
|
338
|
+
json_code="[1.5, 0, 2.5]",
|
|
303
339
|
),
|
|
304
340
|
),
|
|
305
341
|
globals=globals,
|
|
@@ -338,7 +374,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
338
374
|
point_cls = self.init_test_module()["Point"]
|
|
339
375
|
point_cls.OrMutable
|
|
340
376
|
|
|
341
|
-
def
|
|
377
|
+
def test_primitives_default_values(self):
|
|
342
378
|
primitives_cls = self.init_test_module()["Primitives"]
|
|
343
379
|
a = primitives_cls(
|
|
344
380
|
bool=False,
|
|
@@ -351,29 +387,148 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
351
387
|
t=Timestamp.EPOCH,
|
|
352
388
|
)
|
|
353
389
|
b = primitives_cls()
|
|
390
|
+
self.assertEqual(a, b)
|
|
354
391
|
self.assertEqual(hash(a), hash(b))
|
|
355
392
|
|
|
356
|
-
def
|
|
393
|
+
def test_primitives_to_json(self):
|
|
394
|
+
primitives_cls = self.init_test_module()["Primitives"]
|
|
395
|
+
serializer = primitives_cls.SERIALIZER
|
|
396
|
+
p = primitives_cls(
|
|
397
|
+
bool=True,
|
|
398
|
+
bytes=b"a",
|
|
399
|
+
f32=3.14,
|
|
400
|
+
f64=3.14,
|
|
401
|
+
i32=1,
|
|
402
|
+
i64=2,
|
|
403
|
+
u64=3,
|
|
404
|
+
t=Timestamp.from_unix_millis(4),
|
|
405
|
+
)
|
|
406
|
+
self.assertEqual(serializer.to_json(p), [1, "61", 3.14, 3.14, 1, 2, 3, "", 4])
|
|
407
|
+
|
|
408
|
+
def test_primitives_from_json(self):
|
|
409
|
+
primitives_cls = self.init_test_module()["Primitives"]
|
|
410
|
+
serializer = primitives_cls.SERIALIZER
|
|
411
|
+
json = [0] * 100
|
|
412
|
+
self.assertEqual(serializer.from_json(json), primitives_cls.DEFAULT)
|
|
413
|
+
self.assertEqual(
|
|
414
|
+
serializer.from_json([1, "61", 3.14, 3.14, 1, 2, 3, "", 4]),
|
|
415
|
+
primitives_cls(
|
|
416
|
+
bool=True,
|
|
417
|
+
bytes=b"a",
|
|
418
|
+
f32=3.14,
|
|
419
|
+
f64=3.14,
|
|
420
|
+
i32=1,
|
|
421
|
+
i64=2,
|
|
422
|
+
u64=3,
|
|
423
|
+
t=Timestamp.from_unix_millis(4),
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
def test_primitives_repr(self):
|
|
428
|
+
primitives_cls = self.init_test_module()["Primitives"]
|
|
429
|
+
serializer = primitives_cls.SERIALIZER
|
|
430
|
+
p = primitives_cls(
|
|
431
|
+
bool=True,
|
|
432
|
+
bytes=b"a",
|
|
433
|
+
f32=3.14,
|
|
434
|
+
f64=3.14,
|
|
435
|
+
i32=1,
|
|
436
|
+
i64=2,
|
|
437
|
+
u64=3,
|
|
438
|
+
t=Timestamp.from_unix_millis(4),
|
|
439
|
+
)
|
|
440
|
+
self.assertEqual(
|
|
441
|
+
str(p),
|
|
442
|
+
"Primitives(\n bool=True,\n bytes=b'a',\n f32=3.14,\n f64=3.14,\n i32=1,\n i64=2,\n u64=3,\n t=Timestamp(\n unix_millis=4,\n _formatted='1970-01-01T00:00:00.004000Z',\n ),\n)",
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def test_from_json_converts_between_ints_and_floats(self):
|
|
446
|
+
primitives_cls = self.init_test_module()["Primitives"]
|
|
447
|
+
serializer = primitives_cls.SERIALIZER
|
|
448
|
+
p = serializer.from_json([0, 0, 3])
|
|
449
|
+
self.assertEqual(p.f32, 3.0)
|
|
450
|
+
self.assertIsInstance(p.f32, float)
|
|
451
|
+
p = serializer.from_json({"i32": 1.2})
|
|
452
|
+
self.assertEqual(p.i32, 1)
|
|
453
|
+
self.assertIsInstance(p.i32, int)
|
|
454
|
+
|
|
455
|
+
def test_cannot_mutate_frozen_class(self):
|
|
357
456
|
point_cls = self.init_test_module()["Point"]
|
|
457
|
+
serializer = point_cls.SERIALIZER
|
|
358
458
|
point = point_cls(x=1.5, y=2.5)
|
|
359
|
-
|
|
360
|
-
|
|
459
|
+
try:
|
|
460
|
+
point.x = 3.5
|
|
461
|
+
self.fail("expected to raise FrozenInstanceError")
|
|
462
|
+
except dataclasses.FrozenInstanceError:
|
|
463
|
+
pass
|
|
464
|
+
self.assertEqual(point.x, 1.5)
|
|
361
465
|
|
|
362
|
-
def
|
|
466
|
+
def test_point_to_dense_json(self):
|
|
363
467
|
point_cls = self.init_test_module()["Point"]
|
|
468
|
+
serializer = point_cls.SERIALIZER
|
|
364
469
|
point = point_cls(x=1.5, y=2.5)
|
|
365
|
-
|
|
470
|
+
self.assertEqual(serializer.to_json(point), [1.5, 0, 2.5])
|
|
471
|
+
self.assertEqual(serializer.to_json(point, readable=False), [1.5, 0, 2.5])
|
|
472
|
+
self.assertEqual(serializer.to_json_code(point), "[1.5,0,2.5]")
|
|
473
|
+
point = point_cls(x=1.5, y=0.0)
|
|
474
|
+
self.assertEqual(serializer.to_json(point), [1.5])
|
|
475
|
+
|
|
476
|
+
def test_point_to_readable_json(self):
|
|
477
|
+
point_cls = self.init_test_module()["Point"]
|
|
478
|
+
point = point_cls(x=1.5, y=2.5)
|
|
479
|
+
json = point_cls.SERIALIZER.to_json(point, readable=True)
|
|
366
480
|
self.assertEqual(json, {"x": 1.5, "y": 2.5})
|
|
481
|
+
json_code = point_cls.SERIALIZER.to_json_code(point, readable=True)
|
|
482
|
+
self.assertEqual(json_code, '{\n "x": 1.5,\n "y": 2.5\n}')
|
|
367
483
|
|
|
368
|
-
def
|
|
484
|
+
def test_point_from_dense_json(self):
|
|
369
485
|
point_cls = self.init_test_module()["Point"]
|
|
370
|
-
|
|
371
|
-
self.assertEqual(
|
|
486
|
+
serializer = point_cls.SERIALIZER
|
|
487
|
+
self.assertEqual(serializer.from_json([1.5, 0, 2.5]), point_cls(x=1.5, y=2.5))
|
|
488
|
+
self.assertEqual(serializer.from_json([1.5]), point_cls(x=1.5))
|
|
489
|
+
self.assertEqual(serializer.from_json([0.0]), point_cls.DEFAULT)
|
|
490
|
+
self.assertEqual(serializer.from_json(0), point_cls.DEFAULT)
|
|
372
491
|
|
|
373
|
-
def
|
|
492
|
+
def test_point_from_readable_json(self):
|
|
374
493
|
point_cls = self.init_test_module()["Point"]
|
|
375
494
|
point = point_cls.SERIALIZER.from_json({"x": 1.5, "y": 2.5})
|
|
376
495
|
self.assertEqual(point, point_cls(x=1.5, y=2.5))
|
|
496
|
+
point = point_cls.SERIALIZER.from_json_code('{"x":1.5,"y":2.5}')
|
|
497
|
+
self.assertEqual(point, point_cls(x=1.5, y=2.5))
|
|
498
|
+
point = point_cls.SERIALIZER.from_json_code('{"x":1.5,"y":2.5,"z":[]}')
|
|
499
|
+
self.assertEqual(point, point_cls(x=1.5, y=2.5))
|
|
500
|
+
point = point_cls.SERIALIZER.from_json_code('{"x":1,"y":2}')
|
|
501
|
+
self.assertEqual(point.x, 1.0)
|
|
502
|
+
self.assertIsInstance(point.x, float)
|
|
503
|
+
|
|
504
|
+
def test_point_with_unrecognized_and_removed_fields(self):
|
|
505
|
+
point_cls = self.init_test_module()["Point"]
|
|
506
|
+
serializer = point_cls.SERIALIZER
|
|
507
|
+
point = serializer.from_json([1.5, 1, 2.5, True])
|
|
508
|
+
self.assertEqual(point, point_cls(x=1.5, y=2.5))
|
|
509
|
+
self.assertEqual(serializer.to_json(point), [1.5, 0, 2.5, True])
|
|
510
|
+
point = point.to_mutable().to_frozen()
|
|
511
|
+
self.assertEqual(serializer.to_json(point), [1.5, 0, 2.5, True])
|
|
512
|
+
|
|
513
|
+
def test_struct_to_dense_json_with_removed_fields(self):
|
|
514
|
+
test_module = self.init_test_module()
|
|
515
|
+
foobar_cls = test_module["Foobar"]
|
|
516
|
+
point_cls = test_module["Point"]
|
|
517
|
+
serializer = foobar_cls.SERIALIZER
|
|
518
|
+
foobar = foobar_cls()
|
|
519
|
+
self.assertEqual(serializer.to_json_code(foobar), "[]")
|
|
520
|
+
self.assertEqual(serializer.from_json_code("[]"), foobar)
|
|
521
|
+
foobar = foobar_cls(a=3)
|
|
522
|
+
self.assertEqual(serializer.to_json_code(foobar), "[0,3]")
|
|
523
|
+
self.assertEqual(serializer.from_json_code("[0,3]"), foobar)
|
|
524
|
+
self.assertEqual(serializer.from_json_code("[0,3.1]"), foobar)
|
|
525
|
+
foobar = foobar_cls(b=3, point=point_cls.DEFAULT)
|
|
526
|
+
self.assertEqual(serializer.to_json_code(foobar), "[0,0,0,3]")
|
|
527
|
+
self.assertEqual(serializer.from_json_code("[0,0,0,3]"), foobar)
|
|
528
|
+
self.assertEqual(serializer.from_json_code("[0,0,0,3.1]"), foobar)
|
|
529
|
+
foobar = foobar_cls(point=point_cls(x=2))
|
|
530
|
+
self.assertEqual(serializer.to_json_code(foobar), "[0,0,0,0,[2.0]]")
|
|
531
|
+
self.assertEqual(serializer.from_json_code("[0,0,0,0,[2.0]]"), foobar)
|
|
377
532
|
|
|
378
533
|
def test_struct_ctor_accepts_mutable_struct(self):
|
|
379
534
|
module = self.init_test_module()
|
|
@@ -488,7 +643,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
488
643
|
self.assertIs(unknown.union, unknown)
|
|
489
644
|
serializer = primary_color_cls.SERIALIZER
|
|
490
645
|
self.assertEqual(serializer.to_json(unknown), 0)
|
|
491
|
-
self.assertEqual(serializer.to_json(unknown,
|
|
646
|
+
self.assertEqual(serializer.to_json(unknown, readable=True), "?")
|
|
492
647
|
|
|
493
648
|
def test_enum_user_defined_constant(self):
|
|
494
649
|
module = self.init_test_module()
|
|
@@ -499,7 +654,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
499
654
|
self.assertIs(red.union, red)
|
|
500
655
|
serializer = primary_color_cls.SERIALIZER
|
|
501
656
|
self.assertEqual(serializer.to_json(red), 10)
|
|
502
|
-
self.assertEqual(serializer.to_json(red,
|
|
657
|
+
self.assertEqual(serializer.to_json(red, readable=True), "RED")
|
|
503
658
|
|
|
504
659
|
def test_enum_wrap(self):
|
|
505
660
|
module = self.init_test_module()
|
|
@@ -511,7 +666,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
511
666
|
serializer = status_cls.SERIALIZER
|
|
512
667
|
self.assertEqual(serializer.to_json(error), [2, "An error occurred"])
|
|
513
668
|
self.assertEqual(
|
|
514
|
-
serializer.to_json(error,
|
|
669
|
+
serializer.to_json(error, readable=True),
|
|
515
670
|
{"kind": "error", "value": "An error occurred"},
|
|
516
671
|
)
|
|
517
672
|
|
|
@@ -526,6 +681,121 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
526
681
|
json_object, json_value_cls.wrap_object(json_object_cls.DEFAULT)
|
|
527
682
|
)
|
|
528
683
|
|
|
684
|
+
def test_enum_to_json(self):
|
|
685
|
+
module = self.init_test_module()
|
|
686
|
+
status_cls = module["Status"]
|
|
687
|
+
serializer = status_cls.SERIALIZER
|
|
688
|
+
self.assertEqual(serializer.to_json(status_cls.UNKNOWN), 0)
|
|
689
|
+
self.assertEqual(serializer.to_json(status_cls.UNKNOWN, readable=True), "?")
|
|
690
|
+
self.assertEqual(serializer.to_json(status_cls.OK), 1)
|
|
691
|
+
self.assertEqual(serializer.to_json(status_cls.OK, readable=True), "OK")
|
|
692
|
+
self.assertEqual(serializer.to_json(status_cls.OK, readable=False), 1)
|
|
693
|
+
self.assertEqual(serializer.to_json(status_cls.wrap_error("E")), [2, "E"])
|
|
694
|
+
self.assertEqual(
|
|
695
|
+
serializer.to_json(status_cls.wrap_error("E"), readable=True),
|
|
696
|
+
{"kind": "error", "value": "E"},
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
def test_enum_from_json(self):
|
|
700
|
+
module = self.init_test_module()
|
|
701
|
+
status_cls = module["Status"]
|
|
702
|
+
serializer = status_cls.SERIALIZER
|
|
703
|
+
self.assertEqual(serializer.from_json(0), status_cls.UNKNOWN)
|
|
704
|
+
self.assertEqual(serializer.from_json("?"), status_cls.UNKNOWN)
|
|
705
|
+
self.assertEqual(serializer.from_json(1), status_cls.OK)
|
|
706
|
+
self.assertEqual(serializer.from_json("OK"), status_cls.OK)
|
|
707
|
+
self.assertEqual(serializer.from_json([2, "E"]), status_cls.wrap_error("E"))
|
|
708
|
+
self.assertEqual(
|
|
709
|
+
serializer.from_json({"kind": "error", "value": "E"}),
|
|
710
|
+
status_cls.wrap_error("E"),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
def test_complex_enum_from_json(self):
|
|
714
|
+
module = self.init_test_module()
|
|
715
|
+
json_value_cls = module["JsonValue"]
|
|
716
|
+
serializer = json_value_cls.SERIALIZER
|
|
717
|
+
json_value = serializer.from_json(
|
|
718
|
+
[
|
|
719
|
+
5,
|
|
720
|
+
[
|
|
721
|
+
0,
|
|
722
|
+
1,
|
|
723
|
+
[2, True],
|
|
724
|
+
[3, 3.14],
|
|
725
|
+
[4, "foo"],
|
|
726
|
+
[
|
|
727
|
+
6,
|
|
728
|
+
[["a", 0], ["b", 0]],
|
|
729
|
+
],
|
|
730
|
+
[5, [[5, []]]],
|
|
731
|
+
],
|
|
732
|
+
]
|
|
733
|
+
)
|
|
734
|
+
self.assertEqual(
|
|
735
|
+
json_value,
|
|
736
|
+
json_value_cls.wrap_array(
|
|
737
|
+
[
|
|
738
|
+
json_value_cls.UNKNOWN,
|
|
739
|
+
json_value_cls.NULL,
|
|
740
|
+
json_value_cls.wrap_bool(True),
|
|
741
|
+
json_value_cls.wrap_number(3.14),
|
|
742
|
+
json_value_cls.wrap_string("foo"),
|
|
743
|
+
json_value_cls.wrap_object(
|
|
744
|
+
json_value_cls.Object(
|
|
745
|
+
entries=[
|
|
746
|
+
json_value_cls.ObjectEntry(),
|
|
747
|
+
json_value_cls.ObjectEntry.DEFAULT,
|
|
748
|
+
],
|
|
749
|
+
)
|
|
750
|
+
),
|
|
751
|
+
json_value_cls.wrap_array(
|
|
752
|
+
[
|
|
753
|
+
json_value_cls.wrap_array([]),
|
|
754
|
+
]
|
|
755
|
+
),
|
|
756
|
+
]
|
|
757
|
+
),
|
|
758
|
+
)
|
|
759
|
+
self.assertEqual(
|
|
760
|
+
serializer.from_json(
|
|
761
|
+
{
|
|
762
|
+
"kind": "array",
|
|
763
|
+
"value": [
|
|
764
|
+
"?",
|
|
765
|
+
"NULL",
|
|
766
|
+
{"kind": "bool", "value": True},
|
|
767
|
+
{"kind": "number", "value": 3.14},
|
|
768
|
+
{"kind": "string", "value": "foo"},
|
|
769
|
+
{"kind": "object", "value": {"entries": [{}, {}]}},
|
|
770
|
+
{"kind": "array", "value": [{"kind": "array", "value": []}]},
|
|
771
|
+
],
|
|
772
|
+
}
|
|
773
|
+
),
|
|
774
|
+
json_value,
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
def test_struct_with_enum_field(self):
|
|
778
|
+
json_value_cls = self.init_test_module()["JsonValue"]
|
|
779
|
+
json_object_entry_cls = json_value_cls.ObjectEntry
|
|
780
|
+
self.assertEqual(json_object_entry_cls.DEFAULT.value, json_value_cls.UNKNOWN)
|
|
781
|
+
self.assertEqual(json_object_entry_cls().value, json_value_cls.UNKNOWN)
|
|
782
|
+
|
|
783
|
+
def test_enum_with_unrecognized_and_removed_fields(self):
|
|
784
|
+
json_value_cls = self.init_test_module()["JsonValue"]
|
|
785
|
+
serializer = json_value_cls.SERIALIZER
|
|
786
|
+
json_value = serializer.from_json(100) # removed
|
|
787
|
+
self.assertEqual(json_value, json_value_cls.UNKNOWN)
|
|
788
|
+
self.assertEqual(serializer.to_json(json_value), 0)
|
|
789
|
+
json_value = serializer.from_json([101, True]) # removed
|
|
790
|
+
self.assertEqual(json_value, json_value_cls.UNKNOWN)
|
|
791
|
+
self.assertEqual(serializer.to_json(json_value), 0)
|
|
792
|
+
json_value = serializer.from_json(102) # unrecognized
|
|
793
|
+
self.assertEqual(json_value, json_value_cls.UNKNOWN)
|
|
794
|
+
self.assertEqual(serializer.to_json(json_value), 102)
|
|
795
|
+
json_value = serializer.from_json([102, True]) # unrecognized
|
|
796
|
+
self.assertEqual(json_value, json_value_cls.UNKNOWN)
|
|
797
|
+
self.assertEqual(serializer.to_json(json_value), [102, True])
|
|
798
|
+
|
|
529
799
|
def test_class_name(self):
|
|
530
800
|
module = self.init_test_module()
|
|
531
801
|
shape_cls = module["Shape"]
|
|
@@ -822,4 +1092,4 @@ class ModuleInitializerTestCase(unittest.TestCase):
|
|
|
822
1092
|
module = self.init_test_module()
|
|
823
1093
|
c = module["C"]
|
|
824
1094
|
Point = module["Point"]
|
|
825
|
-
self.assertEqual(c, Point(1.5, 2.5))
|
|
1095
|
+
self.assertEqual(c, Point(x=1.5, y=2.5))
|
|
@@ -10,8 +10,15 @@ from soialib import (
|
|
|
10
10
|
|
|
11
11
|
class TimestampTestCase(unittest.TestCase):
|
|
12
12
|
def test_primitive_serializers(self):
|
|
13
|
-
self.assertEqual(primitive_serializer("bool").to_json_code(True), "
|
|
13
|
+
self.assertEqual(primitive_serializer("bool").to_json_code(True), "1")
|
|
14
|
+
self.assertEqual(
|
|
15
|
+
primitive_serializer("bool").to_json_code(True, readable=True),
|
|
16
|
+
"true",
|
|
17
|
+
)
|
|
14
18
|
self.assertEqual(primitive_serializer("int32").to_json_code(7), "7")
|
|
19
|
+
self.assertEqual(
|
|
20
|
+
primitive_serializer("int32").to_json_code(7, readable=True), "7"
|
|
21
|
+
)
|
|
15
22
|
self.assertEqual(
|
|
16
23
|
primitive_serializer("int64").to_json_code(2147483648), "2147483648"
|
|
17
24
|
)
|
|
@@ -21,24 +28,46 @@ class TimestampTestCase(unittest.TestCase):
|
|
|
21
28
|
)
|
|
22
29
|
self.assertEqual(primitive_serializer("float32").to_json_code(3.14), "3.14")
|
|
23
30
|
self.assertEqual(primitive_serializer("float64").to_json_code(3.14), "3.14")
|
|
31
|
+
self.assertEqual(
|
|
32
|
+
primitive_serializer("float64").to_json_code(3.14, readable=True),
|
|
33
|
+
"3.14",
|
|
34
|
+
)
|
|
24
35
|
self.assertEqual(
|
|
25
36
|
primitive_serializer("timestamp").to_json_code(
|
|
26
37
|
Timestamp.from_unix_millis(3)
|
|
27
38
|
),
|
|
28
39
|
"3",
|
|
29
40
|
)
|
|
41
|
+
self.assertEqual(
|
|
42
|
+
primitive_serializer("timestamp").to_json_code(
|
|
43
|
+
Timestamp.from_unix_millis(3), readable=True
|
|
44
|
+
),
|
|
45
|
+
'{\n "unix_millis": 3,\n "formatted": "1970-01-01T00:00:00.003000Z"\n}',
|
|
46
|
+
)
|
|
30
47
|
self.assertEqual(primitive_serializer("string").to_json_code("foo"), '"foo"')
|
|
31
48
|
self.assertEqual(primitive_serializer("bytes").to_json_code(b"foo"), '"666f6f"')
|
|
32
49
|
|
|
33
50
|
def test_array_serializer(self):
|
|
34
51
|
self.assertEqual(
|
|
35
|
-
array_serializer(primitive_serializer("bool")).to_json_code(
|
|
36
|
-
"[
|
|
52
|
+
array_serializer(primitive_serializer("bool")).to_json_code((True, False)),
|
|
53
|
+
"[1,0]",
|
|
54
|
+
)
|
|
55
|
+
self.assertEqual(
|
|
56
|
+
array_serializer(primitive_serializer("bool")).to_json_code(
|
|
57
|
+
(True, False), readable=True
|
|
58
|
+
),
|
|
59
|
+
"[\n true,\n false\n]",
|
|
37
60
|
)
|
|
38
61
|
|
|
39
62
|
def test_optional_serializer(self):
|
|
40
63
|
self.assertEqual(
|
|
41
64
|
optional_serializer(primitive_serializer("bool")).to_json_code(True),
|
|
65
|
+
"1",
|
|
66
|
+
)
|
|
67
|
+
self.assertEqual(
|
|
68
|
+
optional_serializer(primitive_serializer("bool")).to_json_code(
|
|
69
|
+
True, readable=True
|
|
70
|
+
),
|
|
42
71
|
"true",
|
|
43
72
|
)
|
|
44
73
|
self.assertEqual(
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from struct import pack
|
|
2
|
-
from typing import Final
|
|
3
|
-
|
|
4
|
-
from soialib.impl.function_maker import Expr
|
|
5
|
-
|
|
6
|
-
EMPTY_STRING_WIRE: Final[Expr] = Expr.local("EMPTY_STRING_WIRE", bytes([242]))
|
|
7
|
-
STRING_WIRE: Final[Expr] = Expr.local("STRING_WIRE", bytes([243]))
|
|
8
|
-
EMPTY_BYTE_STRING_WIRE: Final[Expr] = Expr.local("EMPTY_BYTE_STRING_WIRE", bytes([244]))
|
|
9
|
-
BYTE_STRING_WIRE: Final[Expr] = Expr.local("BYTE_STRING_WIRE", bytes([245]))
|
|
10
|
-
SMALL_ARRAY_WIRES: Final[Expr] = Expr.local(
|
|
11
|
-
"SMALL_ARRAY_WIRES", tuple(bytes(b) for b in range(246, 250))
|
|
12
|
-
)
|
|
13
|
-
ARRAY_WIRE: Final[Expr] = Expr.local("ARRAY_WIRE", bytes(250))
|
|
14
|
-
NULL_WIRE: Final[Expr] = Expr.local("NULL_WIRE", bytes([255]))
|
|
15
|
-
|
|
16
|
-
_LOW_INT_BYTES: Final[tuple[bytes, ...]] = tuple([bytes([i]) for i in range(232)])
|
|
17
|
-
_ZERO_BYTES: Final[bytes] = _LOW_INT_BYTES[0]
|
|
18
|
-
_ONE_BYTES: Final[bytes] = _LOW_INT_BYTES[1]
|
|
19
|
-
|
|
20
|
-
LOW_INT_BYTES: Final[Expr] = Expr.local("LOW_INT_BYTES", _LOW_INT_BYTES)
|
|
21
|
-
ZERO_BYTES: Final[Expr] = Expr.local("ZERO_BYTES_LOCAL", _ZERO_BYTES)
|
|
22
|
-
ONE_BYTES: Final[Expr] = Expr.local("ONE_BYTES_LOCAL", _ONE_BYTES)
|
|
23
|
-
|
|
24
|
-
PACK: Final[Expr] = Expr.local("pack", pack)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _len_bytes_high(l: int) -> bytes:
|
|
28
|
-
if l < 65536:
|
|
29
|
-
return pack("H", l)
|
|
30
|
-
elif l < 2147483648:
|
|
31
|
-
return pack("I", l)
|
|
32
|
-
raise OverflowError(f"len={l}")
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
LEN_BYTES: Final[Expr] = Expr.join(
|
|
36
|
-
"(",
|
|
37
|
-
LOW_INT_BYTES,
|
|
38
|
-
"[l] if l < 232 else ",
|
|
39
|
-
Expr.local("len_bytes_high", _len_bytes_high),
|
|
40
|
-
"(l))",
|
|
41
|
-
)
|
|
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
|