soia-client 1.1.5__py3-none-any.whl → 1.1.7__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.

Potentially problematic release.


This version of soia-client might be problematic. Click here for more details.

soia/_impl/structs.py CHANGED
@@ -1,9 +1,11 @@
1
1
  import copy
2
+ import itertools
2
3
  from collections.abc import Callable, Sequence
3
4
  from dataclasses import FrozenInstanceError, dataclass
4
- from typing import Any, Final, Union, cast
5
+ from typing import Any, Final, Generic, Union, cast
5
6
 
6
7
  from soia import _spec, reflection
8
+ from soia._impl.binary import decode_int64, decode_unused, encode_length_prefix
7
9
  from soia._impl.function_maker import (
8
10
  BodyBuilder,
9
11
  Expr,
@@ -17,10 +19,10 @@ from soia._impl.function_maker import (
17
19
  )
18
20
  from soia._impl.keep import KEEP
19
21
  from soia._impl.repr import repr_impl
20
- from soia._impl.type_adapter import TypeAdapter
22
+ from soia._impl.type_adapter import ByteStream, T, TypeAdapter
21
23
 
22
24
 
23
- class StructAdapter(TypeAdapter):
25
+ class StructAdapter(Generic[T], TypeAdapter[T]):
24
26
  __slots__ = (
25
27
  "spec",
26
28
  "record_hash",
@@ -43,6 +45,8 @@ class StructAdapter(TypeAdapter):
43
45
  # 0: has not started; 1: in progress; 2: done
44
46
  finalization_state: int
45
47
  fields: tuple["_Field", ...]
48
+ num_slots_incl_removed: int
49
+ slot_to_field: list["_Field | None"]
46
50
 
47
51
  def __init__(self, spec: _spec.Struct):
48
52
  self.finalization_state = 0
@@ -82,6 +86,17 @@ class StructAdapter(TypeAdapter):
82
86
  # Reason why it's faster: we don't need to call object.__setattr__().
83
87
  self.simple_class = _make_dataclass(slots)
84
88
 
89
+ def forward_encode(value: Any, buffer: bytearray) -> None:
90
+ frozen_class._encode(value, buffer)
91
+
92
+ # Will be overridden at finalization time.
93
+ frozen_class._encode = forward_encode
94
+
95
+ def forward_decode(stream: ByteStream) -> None:
96
+ return frozen_class._decode(stream)
97
+
98
+ frozen_class._decode = forward_decode
99
+
85
100
  def finalize(
86
101
  self,
87
102
  resolve_type_fn: Callable[[_spec.Type], TypeAdapter],
@@ -100,6 +115,18 @@ class StructAdapter(TypeAdapter):
100
115
  )
101
116
  )
102
117
 
118
+ num_slots_incl_removed: Final = max(
119
+ itertools.chain(
120
+ (f.field.number + 1 for f in fields),
121
+ (n + 1 for n in self.spec.removed_numbers),
122
+ [0],
123
+ )
124
+ )
125
+ self.num_slots_incl_removed = num_slots_incl_removed
126
+
127
+ slot_to_field: Final[list[_Field | None]] = [None] * self.num_slots_incl_removed
128
+ self.slot_to_field = slot_to_field
129
+
103
130
  # Aim to have dependencies finalized *before* the dependent. It's not always
104
131
  # possible, because there can be cyclic dependencies.
105
132
  # The function returned by the do_x_fn() method of a dependency is marginally
@@ -107,6 +134,7 @@ class StructAdapter(TypeAdapter):
107
134
  # this function is a "forwarding" function.
108
135
  for field in fields:
109
136
  field.type.finalize(resolve_type_fn)
137
+ self.slot_to_field[field.field.number] = field
110
138
 
111
139
  frozen_class = self.gen_class
112
140
  mutable_class = self.mutable_class
@@ -147,13 +175,29 @@ class StructAdapter(TypeAdapter):
147
175
  Any, _make_repr_fn(fields)
148
176
  )
149
177
 
150
- frozen_class._tdj = _make_to_dense_json_fn(fields=fields)
178
+ frozen_class._tdj = _make_to_dense_json_fn(
179
+ fields=fields,
180
+ num_slots_incl_removed=num_slots_incl_removed,
181
+ )
151
182
  frozen_class._trj = _make_to_readable_json_fn(fields=fields)
183
+ frozen_class._encode = _make_encode_fn(
184
+ fields=fields,
185
+ num_slots_incl_removed=num_slots_incl_removed,
186
+ )
187
+ frozen_class._decode = _make_decode_fn(
188
+ frozen_class=frozen_class,
189
+ simple_class=simple_class,
190
+ fields=fields,
191
+ removed_numbers=self.spec.removed_numbers,
192
+ num_slots_incl_removed=num_slots_incl_removed,
193
+ )
194
+
152
195
  frozen_class._fj = _make_from_json_fn(
153
196
  frozen_class=frozen_class,
154
197
  simple_class=simple_class,
155
198
  fields=fields,
156
199
  removed_numbers=self.spec.removed_numbers,
200
+ num_slots_incl_removed=self.num_slots_incl_removed,
157
201
  )
158
202
 
159
203
  # Initialize DEFAULT.
@@ -186,7 +230,13 @@ class StructAdapter(TypeAdapter):
186
230
  )
187
231
 
188
232
  def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> Expr:
189
- return Expr.join(attr_expr, "._array_len")
233
+ return Expr.join(
234
+ "(",
235
+ attr_expr,
236
+ "._unrecognized or ",
237
+ attr_expr,
238
+ "._array_len)",
239
+ )
190
240
 
191
241
  def to_json_expr(
192
242
  self,
@@ -195,15 +245,29 @@ class StructAdapter(TypeAdapter):
195
245
  ) -> Expr:
196
246
  return Expr.join(in_expr, "._trj" if readable else "._tdj", "()")
197
247
 
198
- def from_json_expr(self, json_expr: ExprLike) -> Expr:
248
+ def from_json_expr(
249
+ self, json_expr: ExprLike, keep_unrecognized_expr: ExprLike
250
+ ) -> Expr:
199
251
  fn_name = "_fj"
200
252
  # The _fj method may not have been added to the class yet.
201
253
  from_json_fn = getattr(self.gen_class, fn_name, None)
202
254
  if from_json_fn:
203
- return Expr.join(Expr.local("_fj?", from_json_fn), "(", json_expr, ")")
255
+ return Expr.join(
256
+ Expr.local("_fj?", from_json_fn),
257
+ "(",
258
+ json_expr,
259
+ ", ",
260
+ keep_unrecognized_expr,
261
+ ")",
262
+ )
204
263
  else:
205
264
  return Expr.join(
206
- Expr.local("_cls?", self.gen_class), f".{fn_name}(", json_expr, ")"
265
+ Expr.local("_cls?", self.gen_class),
266
+ f".{fn_name}(",
267
+ json_expr,
268
+ ", ",
269
+ keep_unrecognized_expr,
270
+ ")",
207
271
  )
208
272
 
209
273
  def get_type(self) -> reflection.Type:
@@ -235,9 +299,15 @@ class StructAdapter(TypeAdapter):
235
299
  for field in self.fields:
236
300
  field.type.register_records(registry)
237
301
 
238
- def frozen_class_of_struct(self) -> type | None:
302
+ def frozen_class_of_struct(self) -> type:
239
303
  return self.gen_class
240
304
 
305
+ def encode_fn(self) -> Callable[[T, bytearray], None]:
306
+ return self.gen_class._encode
307
+
308
+ def decode_fn(self) -> Callable[[ByteStream], T]:
309
+ return self.gen_class._decode
310
+
241
311
 
242
312
  class _Frozen:
243
313
  def __setattr__(self, name: str, value: Any):
@@ -316,7 +386,7 @@ def _make_frozen_class_init_fn(
316
386
  ")",
317
387
  )
318
388
  # Set the _unrecognized field.
319
- builder.append_ln(obj_setattr, '(_self, "_unrecognized", ())')
389
+ builder.append_ln(obj_setattr, '(_self, "_unrecognized", None)')
320
390
  # Set array length.
321
391
  builder.append_ln(
322
392
  obj_setattr,
@@ -343,7 +413,7 @@ def _make_frozen_class_init_fn(
343
413
  field.type.to_frozen_expr(attribute),
344
414
  )
345
415
  # Set the _unrecognized field.
346
- builder.append_ln("_self._unrecognized = ()")
416
+ builder.append_ln("_self._unrecognized = None")
347
417
  # Set array length.
348
418
  builder.append_ln("_self._array_len = ", array_len_expr())
349
419
  # Change back the __class__.
@@ -377,7 +447,7 @@ def _make_mutable_class_init_fn(fields: Sequence[_Field]) -> Callable[..., None]
377
447
  " = ",
378
448
  attribute,
379
449
  )
380
- builder.append_ln("_self._unrecognized = ()")
450
+ builder.append_ln("_self._unrecognized = None")
381
451
  return make_function(
382
452
  name="__init__",
383
453
  params=params,
@@ -526,26 +596,19 @@ def _make_to_frozen_fn(
526
596
  builder.append_ln("ret._unrecognized = self._unrecognized")
527
597
 
528
598
  def array_len_expr() -> Expr:
529
- spans: list[LineSpanLike] = []
530
- spans.append(
531
- LineSpan.join(
532
- f"{_get_num_slots(fields)} + ",
533
- Expr.local("_len", len),
534
- "(ret._unrecognized) if ret._unrecognized else ",
535
- )
536
- )
599
+ exprs: list[ExprLike] = []
537
600
  # Fields are sorted by number.
538
601
  for field in reversed(fields):
539
602
  attr_expr = f"ret.{field.field.attribute}"
540
- spans.append(
603
+ exprs.append(
541
604
  LineSpan.join(
542
605
  f"{field.field.number + 1} if ",
543
606
  field.type.is_not_default_expr(attr_expr, attr_expr),
544
607
  " else ",
545
608
  )
546
609
  )
547
- spans.append("0")
548
- return Expr.join(*spans)
610
+ exprs.append("0")
611
+ return Expr.join(*exprs)
549
612
 
550
613
  # Set the _unrecognized field.
551
614
  builder.append_ln("ret._unrecognized = self._unrecognized")
@@ -577,8 +640,14 @@ def _make_eq_fn(
577
640
  builder.append_ln("if other.__class__ is self.__class__:")
578
641
  operands: list[ExprLike]
579
642
  if fields:
580
- attr: Callable[[_Field], str] = lambda f: f.field.attribute
581
- operands = [Expr.join(f"self.{attr(f)} == other.{attr(f)}") for f in fields]
643
+
644
+ def get_attribute(f: _Field) -> str:
645
+ return f.field.attribute
646
+
647
+ operands = [
648
+ Expr.join(f"self.{get_attribute(f)} == other.{get_attribute(f)}")
649
+ for f in fields
650
+ ]
582
651
  else:
583
652
  operands = ["True"]
584
653
  builder.append_ln(" return ", Expr.join(*operands, separator=" and "))
@@ -641,9 +710,13 @@ def _make_repr_fn(fields: Sequence[_Field]) -> Callable[[Any], str]:
641
710
  )
642
711
 
643
712
 
644
- def _make_to_dense_json_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
713
+ def _make_to_dense_json_fn(
714
+ fields: Sequence[_Field], num_slots_incl_removed: int
715
+ ) -> Callable[[Any], Any]:
645
716
  builder = BodyBuilder()
646
- builder.append_ln("l = self._array_len")
717
+ builder.append_ln(
718
+ "l = (self._unrecognized.adjusted_json_array_len if self._unrecognized else None) or self._array_len"
719
+ )
647
720
  builder.append_ln("ret = [0] * l")
648
721
  for field in fields:
649
722
  builder.append_ln(f"if l <= {field.field.number}:")
@@ -655,10 +728,8 @@ def _make_to_dense_json_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
655
728
  readable=False,
656
729
  ),
657
730
  )
658
- num_slots = _get_num_slots(fields)
659
- builder.append_ln(f"if l <= {num_slots}:")
660
- builder.append_ln(" return ret")
661
- builder.append_ln(f"ret[{num_slots}:] = self._unrecognized")
731
+ builder.append_ln(f"if {num_slots_incl_removed} < l:")
732
+ builder.append_ln(f" ret[{num_slots_incl_removed}:] = self._unrecognized.json")
662
733
  builder.append_ln("return ret")
663
734
  return make_function(
664
735
  name="to_dense_json",
@@ -692,11 +763,60 @@ def _make_to_readable_json_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
692
763
  )
693
764
 
694
765
 
766
+ def _make_encode_fn(
767
+ fields: Sequence[_Field],
768
+ num_slots_incl_removed: int,
769
+ ) -> Callable[[Any, bytearray], None]:
770
+ builder = BodyBuilder()
771
+ builder.append_ln(
772
+ "l = (value._unrecognized.adjusted_bytes_array_len if ",
773
+ "value._unrecognized else None) or value._array_len",
774
+ )
775
+ builder.append_ln("if l < 4:")
776
+ builder.append_ln(" buffer.append(246 + l)")
777
+ builder.append_ln("else:")
778
+ builder.append_ln(" buffer.append(250)")
779
+ builder.append_ln(
780
+ " ",
781
+ Expr.local("_encode_length_prefix", encode_length_prefix),
782
+ "(l, buffer)",
783
+ )
784
+ last_number = -1
785
+ for field in fields:
786
+ number = field.field.number
787
+ builder.append_ln(f"if l <= {number}:")
788
+ builder.append_ln(" return")
789
+ missing_slots = number - last_number - 1
790
+ if missing_slots >= 1:
791
+ # There are removed fields between last_number and number.
792
+ zeros = "\\0" * (missing_slots)
793
+ builder.append_ln(f"buffer.extend(b'{zeros}')")
794
+ builder.append_ln(
795
+ Expr.local("encode?", field.type.encode_fn()),
796
+ f"(value.{field.field.attribute}, buffer)",
797
+ )
798
+ last_number = number
799
+ builder.append_ln(f"if l <= {num_slots_incl_removed}:")
800
+ builder.append_ln(" return")
801
+ num_slots_excl_removed = last_number + 1
802
+ if num_slots_excl_removed < num_slots_incl_removed:
803
+ missing_slots = num_slots_incl_removed - num_slots_excl_removed
804
+ zeros = "\\0" * (missing_slots)
805
+ builder.append_ln(f"buffer.extend(b'{zeros}')")
806
+ builder.append_ln("buffer.extend(value._unrecognized.raw_bytes)")
807
+ return make_function(
808
+ name="encode",
809
+ params=["value, buffer"],
810
+ body=builder.build(),
811
+ )
812
+
813
+
695
814
  def _make_from_json_fn(
696
815
  frozen_class: type,
697
816
  simple_class: type,
698
817
  fields: Sequence[_Field],
699
818
  removed_numbers: tuple[int, ...],
819
+ num_slots_incl_removed: int,
700
820
  ) -> Callable[[Any], Any]:
701
821
  builder = BodyBuilder()
702
822
  builder.append_ln("if not json:")
@@ -716,29 +836,39 @@ def _make_from_json_fn(
716
836
  "(json)",
717
837
  )
718
838
  for field in fields:
719
- name = field.field.name
720
839
  number = field.field.number
721
840
  item_expr = f"json[{number}]"
722
841
  builder.append_ln(
723
842
  f" ret.{field.field.attribute} = ",
724
843
  field.type.default_expr(),
725
844
  f" if array_len <= {number} else ",
726
- field.type.from_json_expr(item_expr),
845
+ field.type.from_json_expr(item_expr, "keep_unrecognized_fields"),
727
846
  )
728
- num_slots = _get_num_slots(fields)
729
- builder.append_ln(f" if array_len <= {num_slots}:")
730
- builder.append_ln(" ret._unrecognized = ()")
847
+ if fields:
848
+ num_slots_excl_removed = fields[-1].field.number + 1
849
+ else:
850
+ num_slots_excl_removed = 0
851
+ builder.append_ln(
852
+ f" if array_len <= {num_slots_incl_removed} or not keep_unrecognized_fields:"
853
+ )
854
+ builder.append_ln(" ret._unrecognized = None")
731
855
  builder.append_ln(
732
856
  " ret._array_len = ",
733
- _adjust_array_len_expr("array_len", removed_numbers),
857
+ _adjust_array_len_expr(
858
+ "array_len",
859
+ removed_numbers=removed_numbers,
860
+ num_slots_excl_removed=num_slots_excl_removed,
861
+ ),
734
862
  )
735
863
  builder.append_ln(" else:")
736
864
  builder.append_ln(
737
865
  " ret._unrecognized = ",
866
+ Expr.local("UnrecognizedFields", _UnrecognizedFields.from_json),
867
+ "(json=",
738
868
  Expr.local("deepcopy", copy.deepcopy),
739
- f"(json[{num_slots}:])",
869
+ f"(json[{num_slots_incl_removed}:]), adjusted_json_array_len=array_len)",
740
870
  )
741
- builder.append_ln(" ret._array_len = array_len")
871
+ builder.append_ln(f" ret._array_len = {num_slots_excl_removed}")
742
872
 
743
873
  builder.append_ln("else:")
744
874
  builder.append_ln(" array_len = 0")
@@ -750,12 +880,12 @@ def _make_from_json_fn(
750
880
  builder.append_ln(f" array_len = {field.field.number + 1}")
751
881
  builder.append_ln(
752
882
  f" {lvalue} = ",
753
- field.type.from_json_expr(f'json["{name}"]'),
883
+ field.type.from_json_expr(f'json["{name}"]', "keep_unrecognized_fields"),
754
884
  )
755
885
  builder.append_ln(" else:")
756
886
  builder.append_ln(f" {lvalue} = ", field.type.default_expr())
757
887
  # Drop unrecognized fields in readable mode.
758
- builder.append_ln(" ret._unrecognized = ()")
888
+ builder.append_ln(" ret._unrecognized = None")
759
889
  builder.append_ln(" ret._array_len = array_len")
760
890
 
761
891
  builder.append_ln("ret.__class__ = ", Expr.local("Frozen", frozen_class))
@@ -763,60 +893,123 @@ def _make_from_json_fn(
763
893
 
764
894
  return make_function(
765
895
  name="from_json",
766
- params=["json"],
896
+ params=["json", "keep_unrecognized_fields"],
767
897
  body=builder.build(),
768
898
  )
769
899
 
770
900
 
771
- def _adjust_array_len_expr(var: str, removed_numbers: tuple[int, ...]) -> str:
901
+ def _make_decode_fn(
902
+ frozen_class: type,
903
+ simple_class: type,
904
+ fields: Sequence[_Field],
905
+ removed_numbers: tuple[int, ...],
906
+ num_slots_incl_removed: int,
907
+ ) -> Callable[[ByteStream], Any]:
908
+ builder = BodyBuilder()
909
+ builder.append_ln("wire = stream.buffer[stream.position]")
910
+ builder.append_ln("stream.position += 1")
911
+ builder.append_ln("if wire in (0, 246):")
912
+ builder.append_ln(" return ", Expr.local("DEFAULT", frozen_class.DEFAULT))
913
+ builder.append_ln("elif wire == 250:")
914
+ builder.append_ln(
915
+ " array_len = ",
916
+ Expr.local("decode_int64", decode_int64),
917
+ "(stream)",
918
+ )
919
+ builder.append_ln("else:")
920
+ builder.append_ln(" array_len = wire - 246")
921
+ # Create an instance of the simple class. We'll later change its __class__ attr.
922
+ builder.append_ln(
923
+ "ret = ",
924
+ Expr.local("Simple", simple_class),
925
+ "()",
926
+ )
927
+ last_number = -1
928
+ for field in fields:
929
+ number = field.field.number
930
+ for removed_number in range(last_number + 1, number):
931
+ builder.append_ln(f"if {removed_number} < array_len:")
932
+ builder.append_ln(
933
+ " ",
934
+ Expr.local("decode_unused", decode_unused),
935
+ "(stream)",
936
+ )
937
+ builder.append_ln(
938
+ f"ret.{field.field.attribute} = ",
939
+ field.type.default_expr(),
940
+ f" if array_len <= {number} else ",
941
+ Expr.local("decode?", field.type.decode_fn()),
942
+ "(stream)",
943
+ )
944
+ last_number = number
945
+ num_slots_excl_removed = last_number + 1
946
+
947
+ builder.append_ln(f"if array_len > {num_slots_excl_removed}:")
948
+ builder.append_ln(
949
+ f" if array_len > {num_slots_incl_removed} and keep_unrecognized_fields:"
950
+ )
951
+ for _ in range(num_slots_incl_removed - num_slots_excl_removed):
952
+ builder.append_ln(
953
+ " ", Expr.local("decode_unused", decode_unused), "(stream)"
954
+ )
955
+ builder.append_ln(" start_offset = stream.position")
956
+ builder.append_ln(" for _ in range(array_len - {num_slots_incl_removed}):")
957
+ builder.append_ln(" ", Expr.local("decode_unused", decode_unused), "(stream)")
958
+ builder.append_ln(" end_offset = stream.position")
959
+ builder.append_ln(
960
+ " ret._unrecognized = ",
961
+ Expr.local("UnrecognizedFields", _UnrecognizedFields.from_bytes),
962
+ "(raw_bytes=stream.buffer[start_offset:end_offset], adjusted_bytes_array_len=array_len)",
963
+ )
964
+ builder.append_ln(" else:")
965
+ builder.append_ln(f" for _ in range(array_len - {num_slots_excl_removed}):")
966
+ builder.append_ln(" ", Expr.local("decode_unused", decode_unused), "(stream)")
967
+ builder.append_ln(" ret._unrecognized = None")
968
+ builder.append_ln("else:")
969
+ builder.append_ln(" ret._unrecognized = None")
970
+
971
+ builder.append_ln(
972
+ " ret._array_len = ",
973
+ _adjust_array_len_expr(
974
+ "array_len",
975
+ removed_numbers=removed_numbers,
976
+ num_slots_excl_removed=num_slots_excl_removed,
977
+ ),
978
+ )
979
+
980
+ builder.append_ln(
981
+ "ret.__class__ = ",
982
+ Expr.local("Frozen", frozen_class),
983
+ )
984
+ builder.append_ln("return ret")
985
+ return make_function(
986
+ name="decode",
987
+ params=["stream"],
988
+ body=builder.build(),
989
+ )
990
+
991
+
992
+ def _adjust_array_len_expr(
993
+ var: str,
994
+ removed_numbers: tuple[int, ...],
995
+ num_slots_excl_removed: int,
996
+ ) -> str:
772
997
  """
773
998
  When parsing a dense JSON or decoding a binary string, we can reuse the array length
774
999
  in the decoded struct, but we need to account for possibly newly-removed fields. The
775
1000
  last field of the adjusted array length cannot be a removed field.
776
-
777
- Let's imagine that field number 3 was removed from a struct.
778
- This function would return the following expression:
779
- array_len if array_len <= 3 else 3 if array_len == 4 else array_len
780
1001
  """
781
-
782
- @dataclass
783
- class _RemovedSpan:
784
- """Sequence of consecutive removed fields."""
785
-
786
- # Number of the first removed field.
787
- begin: int = 0
788
- # Number after the last removed field.
789
- end: int = 0
790
-
791
- def get_removed_spans() -> list[_RemovedSpan]:
792
- ret: list[_RemovedSpan] = []
793
- for number in sorted(removed_numbers):
794
- last = ret[-1] if ret else None
795
- if last and last.end == number:
796
- last.end = number + 1
797
- else:
798
- ret.append(_RemovedSpan(number, number + 1))
799
- return ret
800
-
801
- removed_spans = get_removed_spans()
802
-
803
- ret = ""
804
- lower_bound = 0
805
- for s in removed_spans:
806
- if s.begin == lower_bound:
807
- ret += f"{s.begin} if {var} <= {s.end} else "
808
- elif s.end == s.begin + 1:
809
- # Similar to the expression in 'else' but uses '==' instead of '<='
810
- ret += (
811
- f"{var} if {var} <= {s.begin} else {s.begin} if {var} == {s.end} else "
812
- )
1002
+ removed_numbers_set = set(removed_numbers)
1003
+ table: list[int] = [0]
1004
+ last_len = 0
1005
+ for i in range(0, num_slots_excl_removed - 1):
1006
+ if i in removed_numbers_set:
1007
+ table.append(last_len)
813
1008
  else:
814
- ret += (
815
- f"{var} if {var} <= {s.begin} else {s.begin} if {var} <= {s.end} else "
816
- )
817
- lower_bound = s.end + 1
818
- ret += var
819
- return ret
1009
+ table.append(i + 1)
1010
+ last_len = i + 1
1011
+ table_tuple = tuple(table)
1012
+ return f"{table_tuple}[{var}] if {var} < {num_slots_excl_removed} else {num_slots_excl_removed}"
820
1013
 
821
1014
 
822
1015
  def _init_default(default: Any, fields: Sequence[_Field]) -> None:
@@ -828,7 +1021,7 @@ def _init_default(default: Any, fields: Sequence[_Field]) -> None:
828
1021
  body=(Line.join("return ", field.type.default_expr()),),
829
1022
  )
830
1023
  object.__setattr__(default, attribute, get_field_default())
831
- object.__setattr__(default, "_unrecognized", ())
1024
+ object.__setattr__(default, "_unrecognized", None)
832
1025
  object.__setattr__(default, "_array_len", 0)
833
1026
 
834
1027
 
@@ -876,5 +1069,38 @@ def _name_private_is_frozen_attr(record_id: str) -> str:
876
1069
  return f"_is_{record_name}_{hex_hash}"
877
1070
 
878
1071
 
879
- def _get_num_slots(fields: Sequence[_Field]) -> int:
880
- return (fields[-1].field.number + 1) if fields else 0
1072
+ @dataclass(frozen=True)
1073
+ class _UnrecognizedFields:
1074
+ __slots__ = (
1075
+ "json",
1076
+ "raw_bytes",
1077
+ "adjusted_json_array_len",
1078
+ "adjusted_bytes_array_len",
1079
+ )
1080
+
1081
+ json: list[Any] | None
1082
+ raw_bytes: bytes | None
1083
+ adjusted_json_array_len: int | None
1084
+ adjusted_bytes_array_len: int | None
1085
+
1086
+ @staticmethod
1087
+ def from_json(
1088
+ json: list[Any], adjusted_json_array_len: int
1089
+ ) -> "_UnrecognizedFields":
1090
+ return _UnrecognizedFields(
1091
+ json=json,
1092
+ raw_bytes=None,
1093
+ adjusted_json_array_len=adjusted_json_array_len,
1094
+ adjusted_bytes_array_len=None,
1095
+ )
1096
+
1097
+ @staticmethod
1098
+ def from_bytes(
1099
+ raw_bytes: bytes, adjusted_bytes_array_len: int
1100
+ ) -> "_UnrecognizedFields":
1101
+ return _UnrecognizedFields(
1102
+ json=None,
1103
+ raw_bytes=raw_bytes,
1104
+ adjusted_json_array_len=None,
1105
+ adjusted_bytes_array_len=adjusted_bytes_array_len,
1106
+ )
soia/_impl/timestamp.py CHANGED
@@ -146,11 +146,10 @@ class Timestamp:
146
146
  dt = self.to_datetime_or_raise()
147
147
  except Exception:
148
148
  return ""
149
- if dt:
150
- ret = dt.isoformat()
151
- bad_suffix = "+00:00"
152
- if ret.endswith(bad_suffix):
153
- ret = ret[0 : -len(bad_suffix)] + "Z"
149
+ ret = dt.isoformat()
150
+ bad_suffix = "+00:00"
151
+ if ret.endswith(bad_suffix):
152
+ ret = ret[0 : -len(bad_suffix)] + "Z"
154
153
  return ret
155
154
 
156
155