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.
@@ -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}"