soia-client 1.0.0__py3-none-any.whl
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.
- soia_client-1.0.0.dist-info/METADATA +12 -0
- soia_client-1.0.0.dist-info/RECORD +21 -0
- soia_client-1.0.0.dist-info/WHEEL +5 -0
- soia_client-1.0.0.dist-info/top_level.txt +1 -0
- soialib/__init__.py +0 -0
- soialib/impl/__init__.py +0 -0
- soialib/impl/arrays.py +163 -0
- soialib/impl/encoding.py +41 -0
- soialib/impl/enums.py +414 -0
- soialib/impl/function_maker.py +196 -0
- soialib/impl/optionals.py +74 -0
- soialib/impl/primitives.py +210 -0
- soialib/impl/repr.py +43 -0
- soialib/impl/structs.py +718 -0
- soialib/impl/type_adapter.py +56 -0
- soialib/keyed_items.py +14 -0
- soialib/module_initializer.py +79 -0
- soialib/never.py +4 -0
- soialib/serializer.py +87 -0
- soialib/spec.py +148 -0
- soialib/timestamp.py +127 -0
soialib/impl/structs.py
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
from collections.abc import Callable, Sequence
|
|
2
|
+
from dataclasses import FrozenInstanceError, dataclass
|
|
3
|
+
from typing import Any, Final, Union
|
|
4
|
+
|
|
5
|
+
from soialib import spec as _spec
|
|
6
|
+
from soialib.impl.encoding import ARRAY_WIRE, LEN_BYTES, SMALL_ARRAY_WIRES
|
|
7
|
+
from soialib.impl.function_maker import (
|
|
8
|
+
BodyBuilder,
|
|
9
|
+
BodySpan,
|
|
10
|
+
Expr,
|
|
11
|
+
ExprLike,
|
|
12
|
+
Line,
|
|
13
|
+
LineSpan,
|
|
14
|
+
LineSpanLike,
|
|
15
|
+
Param,
|
|
16
|
+
Params,
|
|
17
|
+
make_function,
|
|
18
|
+
)
|
|
19
|
+
from soialib.impl.repr import repr_impl
|
|
20
|
+
from soialib.impl.type_adapter import TypeAdapter
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StructAdapter(TypeAdapter):
|
|
24
|
+
__slots__ = (
|
|
25
|
+
"spec",
|
|
26
|
+
"record_hash",
|
|
27
|
+
"gen_class",
|
|
28
|
+
"mutable_class",
|
|
29
|
+
"simple_class",
|
|
30
|
+
"private_is_frozen_attr",
|
|
31
|
+
"finalization_state",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
spec: Final[_spec.Struct]
|
|
35
|
+
record_hash: Final[int]
|
|
36
|
+
# The frozen class.
|
|
37
|
+
gen_class: Final[type]
|
|
38
|
+
mutable_class: Final[type]
|
|
39
|
+
simple_class: Final[type]
|
|
40
|
+
private_is_frozen_attr: Final[str]
|
|
41
|
+
|
|
42
|
+
# 0: has not started; 1: in progress; 2: done
|
|
43
|
+
finalization_state: int
|
|
44
|
+
|
|
45
|
+
def __init__(self, spec: _spec.Struct):
|
|
46
|
+
self.finalization_state = 0
|
|
47
|
+
self.spec = spec
|
|
48
|
+
self.record_hash = hash(spec.id)
|
|
49
|
+
|
|
50
|
+
self.private_is_frozen_attr = _name_private_is_frozen_attr(spec.id)
|
|
51
|
+
|
|
52
|
+
slots = tuple(f.attribute for f in self.spec.fields) + (
|
|
53
|
+
# Unknown fields encountered during deserialization.
|
|
54
|
+
"_unknown",
|
|
55
|
+
# Lowest number greater than the number of every field with a non-default
|
|
56
|
+
# value.
|
|
57
|
+
"_array_len",
|
|
58
|
+
)
|
|
59
|
+
frozen_class = self.gen_class = _make_dataclass(slots)
|
|
60
|
+
frozen_class.__name__ = spec.class_name
|
|
61
|
+
frozen_class.__qualname__ = spec.class_qualname
|
|
62
|
+
frozen_class.__setattr__ = _Frozen.__setattr__
|
|
63
|
+
frozen_class.__delattr__ = _Frozen.__delattr__
|
|
64
|
+
# We haven't added an __init__ method to the frozen class yet, so frozen_class()
|
|
65
|
+
# returns an object with no attribute set. We'll set the attributes of DEFAULT
|
|
66
|
+
# at the finalization step.
|
|
67
|
+
frozen_class.DEFAULT = frozen_class()
|
|
68
|
+
|
|
69
|
+
mutable_class = self.mutable_class = _make_dataclass(slots)
|
|
70
|
+
mutable_class.__name__ = "Mutable"
|
|
71
|
+
mutable_class.__qualname__ = f"{spec.class_qualname}.Mutable"
|
|
72
|
+
frozen_class.Mutable = mutable_class
|
|
73
|
+
frozen_class.OrMutable = Union[frozen_class, mutable_class]
|
|
74
|
+
|
|
75
|
+
# The 'simple' class is a class which only has __slots__ defined.
|
|
76
|
+
# To construct instances of the frozen class from internal implementation,
|
|
77
|
+
# instead of calling the constructor of the frozen class, we use the following
|
|
78
|
+
# technique: create an instance of the simple class, assign all the attributes,
|
|
79
|
+
# then assign the frozen class to the __class__ attribute.
|
|
80
|
+
# Reason why it's faster: we don't need to call object.__setattr__().
|
|
81
|
+
self.simple_class = _make_dataclass(slots)
|
|
82
|
+
|
|
83
|
+
def finalize(
|
|
84
|
+
self,
|
|
85
|
+
resolve_type_fn: Callable[[_spec.Type], TypeAdapter],
|
|
86
|
+
) -> None:
|
|
87
|
+
if self.finalization_state != 0:
|
|
88
|
+
# Finalization is either in progress or done.
|
|
89
|
+
return
|
|
90
|
+
# Mark finalization as in progress.
|
|
91
|
+
self.finalization_state = 1
|
|
92
|
+
|
|
93
|
+
# Resolve the type of every field.
|
|
94
|
+
fields = tuple(
|
|
95
|
+
sorted(
|
|
96
|
+
(_Field(f, resolve_type_fn(f.type)) for f in self.spec.fields),
|
|
97
|
+
key=lambda f: f.field.number,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# TODO: do I even need this?
|
|
102
|
+
# Aim to have dependencies finalized *before* the dependent. It's not always
|
|
103
|
+
# possible, because there can be cyclic dependencies.
|
|
104
|
+
# The function returned by the do_x_fn() method of a dependency is marginally
|
|
105
|
+
# faster if the dependency is finalized. If the dependency is not finalized,
|
|
106
|
+
# this function is a "forwarding" function.
|
|
107
|
+
for field in fields:
|
|
108
|
+
field.type.finalize(resolve_type_fn)
|
|
109
|
+
|
|
110
|
+
frozen_class = self.gen_class
|
|
111
|
+
mutable_class = self.mutable_class
|
|
112
|
+
simple_class = self.simple_class
|
|
113
|
+
|
|
114
|
+
frozen_class.to_frozen = _identity
|
|
115
|
+
frozen_class.to_mutable = _make_to_mutable_fn(
|
|
116
|
+
mutable_class=mutable_class,
|
|
117
|
+
simple_class=simple_class,
|
|
118
|
+
fields=fields,
|
|
119
|
+
)
|
|
120
|
+
mutable_class.to_frozen = _make_to_frozen_fn(
|
|
121
|
+
frozen_class=frozen_class,
|
|
122
|
+
simple_class=simple_class,
|
|
123
|
+
fields=fields,
|
|
124
|
+
)
|
|
125
|
+
setattr(frozen_class, self.private_is_frozen_attr, True)
|
|
126
|
+
setattr(mutable_class, self.private_is_frozen_attr, False)
|
|
127
|
+
|
|
128
|
+
frozen_class.__init__ = _make_frozen_class_init_fn(
|
|
129
|
+
fields,
|
|
130
|
+
frozen_class=frozen_class,
|
|
131
|
+
simple_class=simple_class,
|
|
132
|
+
)
|
|
133
|
+
mutable_class.__init__ = _make_mutable_class_init_fn(fields)
|
|
134
|
+
|
|
135
|
+
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__ = _make_repr_fn(fields)
|
|
138
|
+
|
|
139
|
+
frozen_class._tdj = _make_to_dense_json_fn(fields=fields)
|
|
140
|
+
frozen_class._trj = _make_to_readable_json_fn(fields=fields)
|
|
141
|
+
frozen_class._fj = _make_from_json_fn(
|
|
142
|
+
frozen_class=frozen_class,
|
|
143
|
+
simple_class=simple_class,
|
|
144
|
+
fields=fields,
|
|
145
|
+
removed_numbers=self.spec.removed_numbers,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Initialize DEFAULT.
|
|
149
|
+
self.gen_class.DEFAULT.__init__()
|
|
150
|
+
|
|
151
|
+
# Define mutable getters
|
|
152
|
+
for field in fields:
|
|
153
|
+
if field.field.has_mutable_getter:
|
|
154
|
+
mutable_getter = _make_mutable_getter(field)
|
|
155
|
+
mutable_getter_name = mutable_getter.__name__
|
|
156
|
+
setattr(mutable_class, mutable_getter_name, property(mutable_getter))
|
|
157
|
+
|
|
158
|
+
# Mark finalization as done.
|
|
159
|
+
self.finalization_state = 2
|
|
160
|
+
|
|
161
|
+
def default_expr(self) -> Expr:
|
|
162
|
+
return Expr.local("_d?", self.gen_class.DEFAULT)
|
|
163
|
+
|
|
164
|
+
def to_frozen_expr(self, arg_expr: ExprLike) -> Expr:
|
|
165
|
+
# TODO: comment
|
|
166
|
+
return Expr.join(
|
|
167
|
+
"(",
|
|
168
|
+
arg_expr,
|
|
169
|
+
" if ",
|
|
170
|
+
arg_expr,
|
|
171
|
+
f".{self.private_is_frozen_attr} else ",
|
|
172
|
+
arg_expr,
|
|
173
|
+
".to_frozen())",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> Expr:
|
|
177
|
+
return Expr.join(attr_expr, f"._array_len")
|
|
178
|
+
|
|
179
|
+
def to_json_expr(
|
|
180
|
+
self,
|
|
181
|
+
in_expr: ExprLike,
|
|
182
|
+
readable: bool,
|
|
183
|
+
) -> Expr:
|
|
184
|
+
return Expr.join(in_expr, "._trj" if readable else "._tdj", "()")
|
|
185
|
+
|
|
186
|
+
def from_json_expr(self, json_expr: ExprLike) -> Expr:
|
|
187
|
+
fn_name = "_fj"
|
|
188
|
+
# TODO: comment
|
|
189
|
+
from_json_fn = getattr(self.gen_class, fn_name, None)
|
|
190
|
+
if from_json_fn:
|
|
191
|
+
return Expr.join(Expr.local("_fj?", from_json_fn), "(", json_expr, ")")
|
|
192
|
+
else:
|
|
193
|
+
return Expr.join(
|
|
194
|
+
Expr.local("_cls?", self.gen_class), f".{fn_name}(", json_expr, ")"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class _Frozen:
|
|
199
|
+
def __setattr__(self, name: str, value: Any):
|
|
200
|
+
raise FrozenInstanceError(self.__class__.__qualname__)
|
|
201
|
+
|
|
202
|
+
def __delattr__(self, name: str):
|
|
203
|
+
raise FrozenInstanceError(self.__class__.__qualname__)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _make_dataclass(slots: tuple[str, ...]) -> type:
|
|
207
|
+
class Result:
|
|
208
|
+
__slots__ = slots
|
|
209
|
+
|
|
210
|
+
return Result
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _identity(input: Any) -> Any:
|
|
214
|
+
return input
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass(frozen=True)
|
|
218
|
+
class _Field:
|
|
219
|
+
field: _spec.Field
|
|
220
|
+
type: TypeAdapter
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _make_frozen_class_init_fn(
|
|
224
|
+
fields: Sequence[_Field],
|
|
225
|
+
frozen_class: type,
|
|
226
|
+
simple_class: type,
|
|
227
|
+
) -> Callable[[Any], None]:
|
|
228
|
+
"""
|
|
229
|
+
Returns the implementation of the __init__() method of the frozen class.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
# Set the params.
|
|
233
|
+
params: Params = ["self"]
|
|
234
|
+
for field in fields:
|
|
235
|
+
params.append(
|
|
236
|
+
Param(
|
|
237
|
+
name=field.field.attribute,
|
|
238
|
+
default=field.type.default_expr(),
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
builder = BodyBuilder()
|
|
243
|
+
# Since __setattr__() was overridden to raise errors in order to make the class
|
|
244
|
+
# immutable, the only way to set attributes in the constructor is to call
|
|
245
|
+
# object.__setattr__().
|
|
246
|
+
obj_setattr = Expr.local("_setattr", object.__setattr__)
|
|
247
|
+
|
|
248
|
+
def array_len_expr() -> Expr:
|
|
249
|
+
spans: list[LineSpanLike] = []
|
|
250
|
+
# Fields are sorted by number.
|
|
251
|
+
for field in reversed(fields):
|
|
252
|
+
spans.append(
|
|
253
|
+
LineSpan.join(
|
|
254
|
+
f"{field.field.number + 1} if ",
|
|
255
|
+
field.type.is_not_default_expr(
|
|
256
|
+
arg_expr=field.field.attribute,
|
|
257
|
+
attr_expr=f"self.{field.field.attribute}",
|
|
258
|
+
),
|
|
259
|
+
" else ",
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
spans.append("0")
|
|
263
|
+
return Expr.join(*spans)
|
|
264
|
+
|
|
265
|
+
if len(fields) < 4:
|
|
266
|
+
# If the class has less than 4 fields, it is faster to assign every field with
|
|
267
|
+
# object.__setattr__().
|
|
268
|
+
for field in fields:
|
|
269
|
+
attribute = field.field.attribute
|
|
270
|
+
builder.append_ln(
|
|
271
|
+
obj_setattr,
|
|
272
|
+
'(self, "',
|
|
273
|
+
attribute,
|
|
274
|
+
'", ',
|
|
275
|
+
field.type.to_frozen_expr(attribute),
|
|
276
|
+
")",
|
|
277
|
+
)
|
|
278
|
+
# Set array length.
|
|
279
|
+
builder.append_ln(
|
|
280
|
+
obj_setattr,
|
|
281
|
+
f'(self, "_array_len", ',
|
|
282
|
+
array_len_expr(),
|
|
283
|
+
")",
|
|
284
|
+
)
|
|
285
|
+
else:
|
|
286
|
+
# If the class has 4 fields or more, it is faster to first change the __class__
|
|
287
|
+
# attribute of the object so it's no longer immutable, assign all the attributes
|
|
288
|
+
# with regular assignment, and then change back the __class__ attribute.
|
|
289
|
+
builder.append_ln(
|
|
290
|
+
obj_setattr,
|
|
291
|
+
'(self, "__class__", ',
|
|
292
|
+
Expr.local("Simple", simple_class),
|
|
293
|
+
")",
|
|
294
|
+
)
|
|
295
|
+
for field in fields:
|
|
296
|
+
attribute = field.field.attribute
|
|
297
|
+
builder.append_ln(
|
|
298
|
+
"self.",
|
|
299
|
+
attribute,
|
|
300
|
+
" = ",
|
|
301
|
+
field.type.to_frozen_expr(attribute),
|
|
302
|
+
)
|
|
303
|
+
# Set array length.
|
|
304
|
+
builder.append_ln(f"self._array_len = ", array_len_expr())
|
|
305
|
+
# Change back the __class__.
|
|
306
|
+
builder.append_ln("self.__class__ = ", Expr.local("Frozen", frozen_class))
|
|
307
|
+
|
|
308
|
+
return make_function(
|
|
309
|
+
name="__init__",
|
|
310
|
+
params=params,
|
|
311
|
+
body=builder.build(),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _make_mutable_class_init_fn(fields: Sequence[_Field]) -> Callable[[Any], None]:
|
|
316
|
+
"""
|
|
317
|
+
Returns the implementation of the __init__() method of the mutable class.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
params: Params = ["self"]
|
|
321
|
+
builder = BodyBuilder()
|
|
322
|
+
for field in fields:
|
|
323
|
+
attribute = field.field.attribute
|
|
324
|
+
params.append(
|
|
325
|
+
Param(
|
|
326
|
+
name=attribute,
|
|
327
|
+
default=field.type.default_expr(),
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
builder.append_ln(
|
|
331
|
+
"self.",
|
|
332
|
+
attribute,
|
|
333
|
+
" = ",
|
|
334
|
+
attribute,
|
|
335
|
+
)
|
|
336
|
+
return make_function(
|
|
337
|
+
name="__init__",
|
|
338
|
+
params=params,
|
|
339
|
+
body=builder.build(),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _make_to_mutable_fn(
|
|
344
|
+
mutable_class: type,
|
|
345
|
+
simple_class: type,
|
|
346
|
+
fields: Sequence[_Field],
|
|
347
|
+
) -> Callable[[Any], Any]:
|
|
348
|
+
"""
|
|
349
|
+
Returns the implementation of the to_mutable() method of the frozen class.
|
|
350
|
+
"""
|
|
351
|
+
builder = BodyBuilder()
|
|
352
|
+
# Create an instance of the simple class. We'll later change its __class__ attribute.
|
|
353
|
+
builder.append_ln(
|
|
354
|
+
"ret = ",
|
|
355
|
+
Expr.local("Simple", simple_class),
|
|
356
|
+
"()",
|
|
357
|
+
)
|
|
358
|
+
for field in fields:
|
|
359
|
+
attribute = field.field.attribute
|
|
360
|
+
builder.append_ln("ret.", attribute, " = self.", attribute)
|
|
361
|
+
# TODO: unknown field
|
|
362
|
+
builder.append_ln(
|
|
363
|
+
"ret.__class__ = ",
|
|
364
|
+
Expr.local("mutable_class", mutable_class),
|
|
365
|
+
)
|
|
366
|
+
builder.append_ln("return ret")
|
|
367
|
+
return make_function(
|
|
368
|
+
name="to_mutable",
|
|
369
|
+
params=["self"],
|
|
370
|
+
body=builder.build(),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _make_to_frozen_fn(
|
|
375
|
+
frozen_class: type,
|
|
376
|
+
simple_class: type,
|
|
377
|
+
fields: Sequence[_Field],
|
|
378
|
+
) -> Callable[[Any], Any]:
|
|
379
|
+
"""
|
|
380
|
+
Returns the implementation of the to_frozen() method of the mutable class.
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
builder = BodyBuilder()
|
|
384
|
+
# Create an instance of the simple class. We'll later change its __class__ attribute.
|
|
385
|
+
builder.append_ln(
|
|
386
|
+
"ret = ",
|
|
387
|
+
Expr.local("Simple", simple_class),
|
|
388
|
+
"()",
|
|
389
|
+
)
|
|
390
|
+
for field in fields:
|
|
391
|
+
attribute = field.field.attribute
|
|
392
|
+
builder.append_ln(
|
|
393
|
+
"ret.",
|
|
394
|
+
attribute,
|
|
395
|
+
" = ",
|
|
396
|
+
field.type.to_frozen_expr(f"self.{attribute}"),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# TODO: unknown field
|
|
400
|
+
|
|
401
|
+
def array_len_expr() -> Expr:
|
|
402
|
+
spans: list[LineSpanLike] = []
|
|
403
|
+
# Fields are sorted by number.
|
|
404
|
+
for field in reversed(fields):
|
|
405
|
+
attr_expr = f"ret.{field.field.attribute}"
|
|
406
|
+
spans.append(
|
|
407
|
+
LineSpan.join(
|
|
408
|
+
f"{field.field.number + 1} if ",
|
|
409
|
+
field.type.is_not_default_expr(attr_expr, attr_expr),
|
|
410
|
+
" else ",
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
spans.append("0")
|
|
414
|
+
return Expr.join(*spans)
|
|
415
|
+
|
|
416
|
+
# Set array length.
|
|
417
|
+
builder.append_ln(f"ret._array_len = ", array_len_expr())
|
|
418
|
+
|
|
419
|
+
builder.append_ln(
|
|
420
|
+
"ret.__class__ = ",
|
|
421
|
+
Expr.local("Frozen", frozen_class),
|
|
422
|
+
)
|
|
423
|
+
builder.append_ln("return ret")
|
|
424
|
+
return make_function(
|
|
425
|
+
name="to_frozen",
|
|
426
|
+
params=["self"],
|
|
427
|
+
body=builder.build(),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _make_eq_fn(
|
|
432
|
+
fields: Sequence[_Field],
|
|
433
|
+
) -> Callable[[Any], Any]:
|
|
434
|
+
"""
|
|
435
|
+
Returns the implementation of the __eq__() method of the frozen class.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
builder = BodyBuilder()
|
|
439
|
+
builder.append_ln("if other.__class__ is self.__class__:")
|
|
440
|
+
operands: list[ExprLike]
|
|
441
|
+
if fields:
|
|
442
|
+
attr: Callable[[_Field], str] = lambda f: f.field.attribute
|
|
443
|
+
operands = [Expr.join(f"self.{attr(f)} == other.{attr(f)}") for f in fields]
|
|
444
|
+
else:
|
|
445
|
+
operands = ["True"]
|
|
446
|
+
builder.append_ln(" return ", Expr.join(*operands, separator=" and "))
|
|
447
|
+
builder.append_ln("return ", Expr.local("NotImplemented", NotImplemented))
|
|
448
|
+
return make_function(
|
|
449
|
+
name="__eq__",
|
|
450
|
+
params=["self", "other"],
|
|
451
|
+
body=builder.build(),
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _make_hash_fn(
|
|
456
|
+
fields: Sequence[_Field],
|
|
457
|
+
record_hash: int,
|
|
458
|
+
) -> Callable[[Any], Any]:
|
|
459
|
+
"""
|
|
460
|
+
Returns the implementation of the __hash__() method of the frozen class.
|
|
461
|
+
"""
|
|
462
|
+
|
|
463
|
+
components: list[ExprLike] = []
|
|
464
|
+
# TODO: comment
|
|
465
|
+
components.append(str(record_hash))
|
|
466
|
+
for field in fields:
|
|
467
|
+
components.append(f"self.{field.field.attribute}")
|
|
468
|
+
return make_function(
|
|
469
|
+
name="__hash__",
|
|
470
|
+
params=["self"],
|
|
471
|
+
body=[Line.join("return hash((", Expr.join(*components, separator=", "), "))")],
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _make_repr_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
|
|
476
|
+
"""
|
|
477
|
+
Returns the implementation of the __repr__() method of both the frozen class and the
|
|
478
|
+
mutable class.
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
builder = BodyBuilder()
|
|
482
|
+
builder.append_ln("if self is getattr(self.__class__, 'DEFAULT', None):")
|
|
483
|
+
builder.append_ln(" return f'{self.__class__.__qualname__}.DEFAULT'")
|
|
484
|
+
builder.append_ln("assignments = []")
|
|
485
|
+
builder.append_ln("any_complex = False")
|
|
486
|
+
builder.append_ln("is_mutable = self.__class__.__name__ == 'Mutable'")
|
|
487
|
+
repr_local = Expr.local("repr", repr_impl)
|
|
488
|
+
for field in fields:
|
|
489
|
+
attribute = field.field.attribute
|
|
490
|
+
# TODO: comment, is_not_default_expr only works on a frozen expression...
|
|
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
|
|
492
|
+
to_frozen_expr = field.type.to_frozen_expr(f"(self.{attribute})")
|
|
493
|
+
is_not_default_expr = field.type.is_not_default_expr(
|
|
494
|
+
to_frozen_expr, to_frozen_expr
|
|
495
|
+
)
|
|
496
|
+
builder.append_ln("if is_mutable or ", is_not_default_expr, ":")
|
|
497
|
+
builder.append_ln(" r = ", repr_local, f"(self.{attribute})")
|
|
498
|
+
builder.append_ln(f" assignments.append(f'{attribute}={{r.indented}}')")
|
|
499
|
+
builder.append_ln(" any_complex = any_complex or r.complex")
|
|
500
|
+
builder.append_ln("if len(assignments) <= 1 and not any_complex:")
|
|
501
|
+
builder.append_ln(" body = ''.join(assignments)")
|
|
502
|
+
builder.append_ln("else:")
|
|
503
|
+
builder.append_ln(" body = '\\n' + ''.join(f' {a},\\n' for a in assignments)")
|
|
504
|
+
builder.append_ln("return f'{self.__class__.__qualname__}({body})'")
|
|
505
|
+
|
|
506
|
+
return make_function(
|
|
507
|
+
name="__repr__",
|
|
508
|
+
params=["self"],
|
|
509
|
+
body=builder.build(),
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _make_to_dense_json_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
|
|
514
|
+
builder = BodyBuilder()
|
|
515
|
+
builder.append_ln(f"l = self._array_len")
|
|
516
|
+
builder.append_ln("ret = [0] * l")
|
|
517
|
+
for field in fields:
|
|
518
|
+
builder.append_ln(f"if l <= {field.field.number}:")
|
|
519
|
+
builder.append_ln(" return ret")
|
|
520
|
+
builder.append_ln(
|
|
521
|
+
f"ret[{field.field.number}] = ",
|
|
522
|
+
field.type.to_json_expr(
|
|
523
|
+
f"self.{field.field.attribute}",
|
|
524
|
+
readable=False,
|
|
525
|
+
),
|
|
526
|
+
)
|
|
527
|
+
# TODO: unknown fields
|
|
528
|
+
builder.append_ln("return ret")
|
|
529
|
+
return make_function(
|
|
530
|
+
name="to_dense_json",
|
|
531
|
+
params=["self"],
|
|
532
|
+
body=builder.build(),
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _make_to_readable_json_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
|
|
537
|
+
builder = BodyBuilder()
|
|
538
|
+
builder.append_ln("ret = {}")
|
|
539
|
+
for field in fields:
|
|
540
|
+
attr_expr = f"self.{field.field.attribute}"
|
|
541
|
+
builder.append_ln(
|
|
542
|
+
"if ",
|
|
543
|
+
field.type.is_not_default_expr(attr_expr, attr_expr),
|
|
544
|
+
":",
|
|
545
|
+
)
|
|
546
|
+
builder.append_ln(
|
|
547
|
+
f' ret["{field.field.name}"] = ',
|
|
548
|
+
field.type.to_json_expr(
|
|
549
|
+
attr_expr,
|
|
550
|
+
readable=True,
|
|
551
|
+
),
|
|
552
|
+
)
|
|
553
|
+
# TODO: unknown fields
|
|
554
|
+
builder.append_ln("return ret")
|
|
555
|
+
return make_function(
|
|
556
|
+
name="to_readable_json",
|
|
557
|
+
params=["self"],
|
|
558
|
+
body=builder.build(),
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _make_from_json_fn(
|
|
563
|
+
frozen_class: type,
|
|
564
|
+
simple_class: type,
|
|
565
|
+
fields: Sequence[_Field],
|
|
566
|
+
removed_numbers: tuple[int, ...],
|
|
567
|
+
) -> Callable[[Any], Any]:
|
|
568
|
+
builder = BodyBuilder()
|
|
569
|
+
builder.append_ln("if not json:")
|
|
570
|
+
builder.append_ln(" return ", Expr.local("DEFAULT", frozen_class.DEFAULT))
|
|
571
|
+
builder.append_ln("ret = ", Expr.local("Simple", simple_class), "()")
|
|
572
|
+
builder.append_ln(
|
|
573
|
+
"if ",
|
|
574
|
+
Expr.local("isinstance", isinstance),
|
|
575
|
+
"(json, ",
|
|
576
|
+
Expr.local("list", list),
|
|
577
|
+
"):",
|
|
578
|
+
)
|
|
579
|
+
# JSON array (dense flavor)
|
|
580
|
+
builder.append_ln(
|
|
581
|
+
" array_len = ",
|
|
582
|
+
Expr.local("len", len),
|
|
583
|
+
"(json)",
|
|
584
|
+
)
|
|
585
|
+
for field in fields:
|
|
586
|
+
name = field.field.name
|
|
587
|
+
number = field.field.number
|
|
588
|
+
item_expr = f"json[{number}]"
|
|
589
|
+
builder.append_ln(
|
|
590
|
+
f" ret.{field.field.attribute} = ",
|
|
591
|
+
field.type.default_expr(),
|
|
592
|
+
f" if array_len <= {number} else ",
|
|
593
|
+
field.type.from_json_expr(item_expr),
|
|
594
|
+
)
|
|
595
|
+
builder.append_ln(
|
|
596
|
+
f" ret._array_len = ",
|
|
597
|
+
_adjust_array_len_expr("array_len", removed_numbers),
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
builder.append_ln("else:")
|
|
601
|
+
builder.append_ln(" array_len = 0")
|
|
602
|
+
# JSON object (readable flavor)
|
|
603
|
+
for field in fields:
|
|
604
|
+
name = field.field.name
|
|
605
|
+
lvalue = f"ret.{field.field.attribute}"
|
|
606
|
+
builder.append_ln(f' if "{name}" in json:')
|
|
607
|
+
builder.append_ln(f" array_len = {field.field.number}")
|
|
608
|
+
builder.append_ln(
|
|
609
|
+
f" {lvalue} = ",
|
|
610
|
+
field.type.from_json_expr(Expr.join(f'json["{name}"]')),
|
|
611
|
+
)
|
|
612
|
+
builder.append_ln(" else:")
|
|
613
|
+
builder.append_ln(f" {lvalue} = ", field.type.default_expr())
|
|
614
|
+
builder.append_ln(f" ret._array_len = array_len")
|
|
615
|
+
|
|
616
|
+
builder.append_ln("ret.__class__ = ", Expr.local("Frozen", frozen_class))
|
|
617
|
+
builder.append_ln("return ret")
|
|
618
|
+
|
|
619
|
+
return make_function(
|
|
620
|
+
name="from_json",
|
|
621
|
+
params=["json"],
|
|
622
|
+
body=builder.build(),
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _adjust_array_len_expr(var: str, removed_numbers: tuple[int, ...]) -> str:
|
|
627
|
+
"""
|
|
628
|
+
When parsing a dense JSON or decoding a binary string, we can reuse the array length
|
|
629
|
+
in the decoded struct, but we need to account for possibly newly-removed fields. The
|
|
630
|
+
last field of the adjusted array length cannot be a removed field.
|
|
631
|
+
|
|
632
|
+
Let's imagine that field number 3 was removed from a struct.
|
|
633
|
+
This function would return the following expression:
|
|
634
|
+
array_len if array_len <= 3 else 3 if array_len == 4 else array_len
|
|
635
|
+
"""
|
|
636
|
+
|
|
637
|
+
@dataclass
|
|
638
|
+
class _RemovedSpan:
|
|
639
|
+
"""Sequence of consecutive removed fields."""
|
|
640
|
+
|
|
641
|
+
# Number of the first removed field.
|
|
642
|
+
begin: int = 0
|
|
643
|
+
# Number after the last removed field.
|
|
644
|
+
end: int = 0
|
|
645
|
+
|
|
646
|
+
def get_removed_spans() -> list[_RemovedSpan]:
|
|
647
|
+
ret: list[_RemovedSpan] = []
|
|
648
|
+
for number in sorted(removed_numbers):
|
|
649
|
+
last = ret[-1] if ret else None
|
|
650
|
+
if last and last.end == number:
|
|
651
|
+
last.end = number + 1
|
|
652
|
+
else:
|
|
653
|
+
ret.append(_RemovedSpan(number, number + 1))
|
|
654
|
+
return ret
|
|
655
|
+
|
|
656
|
+
removed_spans = get_removed_spans()
|
|
657
|
+
|
|
658
|
+
ret = ""
|
|
659
|
+
lower_bound = 0
|
|
660
|
+
for s in removed_spans:
|
|
661
|
+
if s.begin == lower_bound:
|
|
662
|
+
ret += f"{s.begin} if {var} <= {s.end} else "
|
|
663
|
+
elif s.end == s.begin + 1:
|
|
664
|
+
# Similar to the expression in 'else' but uses '==' instead of '<='
|
|
665
|
+
ret += (
|
|
666
|
+
f"{var} if {var} <= {s.begin} else {s.begin} if {var} == {s.end} else "
|
|
667
|
+
)
|
|
668
|
+
else:
|
|
669
|
+
ret += (
|
|
670
|
+
f"{var} if {var} <= {s.begin} else {s.begin} if {var} <= {s.end} else "
|
|
671
|
+
)
|
|
672
|
+
lower_bound = s.end + 1
|
|
673
|
+
ret += var
|
|
674
|
+
return ret
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _make_mutable_getter(field: _Field) -> Callable[[Any], Any]:
|
|
678
|
+
# Two cases: the field either has struct type or array type.
|
|
679
|
+
attribute = field.field.attribute
|
|
680
|
+
builder = BodyBuilder()
|
|
681
|
+
if isinstance(field.type, StructAdapter):
|
|
682
|
+
frozen_class = field.type.gen_class
|
|
683
|
+
mutable_class = frozen_class.Mutable
|
|
684
|
+
builder.append_ln(
|
|
685
|
+
f"if self.{attribute}.__class__ is ",
|
|
686
|
+
Expr.local("mutable_class", mutable_class),
|
|
687
|
+
":",
|
|
688
|
+
)
|
|
689
|
+
builder.append_ln(f" return self.{attribute}")
|
|
690
|
+
builder.append_ln(
|
|
691
|
+
f"if self.{attribute}.__class__ is ",
|
|
692
|
+
Expr.local("frozen_class", frozen_class),
|
|
693
|
+
":",
|
|
694
|
+
)
|
|
695
|
+
builder.append_ln(f" self.{attribute} = self.{attribute}.to_mutable()")
|
|
696
|
+
builder.append_ln(f" return self.{attribute}")
|
|
697
|
+
expected = f"{frozen_class.__qualname__} or {mutable_class.__qualname__}"
|
|
698
|
+
found = f"self.{attribute}.__class__.__name__"
|
|
699
|
+
builder.append_ln(
|
|
700
|
+
"raise ",
|
|
701
|
+
Expr.local("TypeError", TypeError),
|
|
702
|
+
f"(f'expected: {expected}; found: {{{found}}}')",
|
|
703
|
+
)
|
|
704
|
+
else:
|
|
705
|
+
builder.append_ln(f"if not isinstance(self.{attribute}, list):")
|
|
706
|
+
builder.append_ln(f" self.{attribute} = list(self.{attribute})")
|
|
707
|
+
builder.append_ln(f"return self.{attribute}")
|
|
708
|
+
return make_function(
|
|
709
|
+
name=f"mutable_{field.field.name}",
|
|
710
|
+
params=["self"],
|
|
711
|
+
body=builder.build(),
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _name_private_is_frozen_attr(record_id: str) -> str:
|
|
716
|
+
record_name = _spec.RecordId.parse(record_id).name
|
|
717
|
+
hex_hash = hex(abs(hash(record_id)))[:6]
|
|
718
|
+
return f"_is_{record_name}_{hex_hash}"
|