soia-client 1.1.4__py3-none-any.whl → 1.1.6__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,8 +1,10 @@
1
1
  import copy
2
2
  from collections.abc import Callable, Sequence
3
3
  from dataclasses import FrozenInstanceError, dataclass
4
- from typing import Any, Final, Union, cast
4
+ import itertools
5
+ from typing import Any, Final, Generic, Union, cast
5
6
 
7
+ from soia import _spec, reflection
6
8
  from soia._impl.function_maker import (
7
9
  BodyBuilder,
8
10
  Expr,
@@ -16,12 +18,11 @@ from soia._impl.function_maker import (
16
18
  )
17
19
  from soia._impl.keep import KEEP
18
20
  from soia._impl.repr import repr_impl
19
- from soia._impl.type_adapter import TypeAdapter
20
-
21
- from soia import _spec, reflection
21
+ from soia._impl.type_adapter import T, ByteStream, TypeAdapter
22
+ from soia._impl.binary import decode_int64, decode_unused, encode_length_prefix
22
23
 
23
24
 
24
- class StructAdapter(TypeAdapter):
25
+ class StructAdapter(Generic[T], TypeAdapter[T]):
25
26
  __slots__ = (
26
27
  "spec",
27
28
  "record_hash",
@@ -44,6 +45,8 @@ class StructAdapter(TypeAdapter):
44
45
  # 0: has not started; 1: in progress; 2: done
45
46
  finalization_state: int
46
47
  fields: tuple["_Field", ...]
48
+ num_slots_incl_removed: int
49
+ slot_to_field: list["_Field | None"]
47
50
 
48
51
  def __init__(self, spec: _spec.Struct):
49
52
  self.finalization_state = 0
@@ -83,6 +86,17 @@ class StructAdapter(TypeAdapter):
83
86
  # Reason why it's faster: we don't need to call object.__setattr__().
84
87
  self.simple_class = _make_dataclass(slots)
85
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
+
86
100
  def finalize(
87
101
  self,
88
102
  resolve_type_fn: Callable[[_spec.Type], TypeAdapter],
@@ -101,6 +115,18 @@ class StructAdapter(TypeAdapter):
101
115
  )
102
116
  )
103
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
+
104
130
  # Aim to have dependencies finalized *before* the dependent. It's not always
105
131
  # possible, because there can be cyclic dependencies.
106
132
  # The function returned by the do_x_fn() method of a dependency is marginally
@@ -108,6 +134,7 @@ class StructAdapter(TypeAdapter):
108
134
  # this function is a "forwarding" function.
109
135
  for field in fields:
110
136
  field.type.finalize(resolve_type_fn)
137
+ self.slot_to_field[field.field.number] = field
111
138
 
112
139
  frozen_class = self.gen_class
113
140
  mutable_class = self.mutable_class
@@ -148,13 +175,29 @@ class StructAdapter(TypeAdapter):
148
175
  Any, _make_repr_fn(fields)
149
176
  )
150
177
 
151
- 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
+ )
152
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
+
153
195
  frozen_class._fj = _make_from_json_fn(
154
196
  frozen_class=frozen_class,
155
197
  simple_class=simple_class,
156
198
  fields=fields,
157
199
  removed_numbers=self.spec.removed_numbers,
200
+ num_slots_incl_removed=self.num_slots_incl_removed,
158
201
  )
159
202
 
160
203
  # Initialize DEFAULT.
@@ -187,7 +230,13 @@ class StructAdapter(TypeAdapter):
187
230
  )
188
231
 
189
232
  def is_not_default_expr(self, arg_expr: ExprLike, attr_expr: ExprLike) -> Expr:
190
- 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
+ )
191
240
 
192
241
  def to_json_expr(
193
242
  self,
@@ -196,15 +245,29 @@ class StructAdapter(TypeAdapter):
196
245
  ) -> Expr:
197
246
  return Expr.join(in_expr, "._trj" if readable else "._tdj", "()")
198
247
 
199
- 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:
200
251
  fn_name = "_fj"
201
252
  # The _fj method may not have been added to the class yet.
202
253
  from_json_fn = getattr(self.gen_class, fn_name, None)
203
254
  if from_json_fn:
204
- 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
+ )
205
263
  else:
206
264
  return Expr.join(
207
- 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
+ ")",
208
271
  )
209
272
 
210
273
  def get_type(self) -> reflection.Type:
@@ -236,6 +299,15 @@ class StructAdapter(TypeAdapter):
236
299
  for field in self.fields:
237
300
  field.type.register_records(registry)
238
301
 
302
+ def frozen_class_of_struct(self) -> type:
303
+ return self.gen_class
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
+
239
311
 
240
312
  class _Frozen:
241
313
  def __setattr__(self, name: str, value: Any):
@@ -314,7 +386,7 @@ def _make_frozen_class_init_fn(
314
386
  ")",
315
387
  )
316
388
  # Set the _unrecognized field.
317
- builder.append_ln(obj_setattr, '(_self, "_unrecognized", ())')
389
+ builder.append_ln(obj_setattr, '(_self, "_unrecognized", None)')
318
390
  # Set array length.
319
391
  builder.append_ln(
320
392
  obj_setattr,
@@ -341,7 +413,7 @@ def _make_frozen_class_init_fn(
341
413
  field.type.to_frozen_expr(attribute),
342
414
  )
343
415
  # Set the _unrecognized field.
344
- builder.append_ln("_self._unrecognized = ()")
416
+ builder.append_ln("_self._unrecognized = None")
345
417
  # Set array length.
346
418
  builder.append_ln("_self._array_len = ", array_len_expr())
347
419
  # Change back the __class__.
@@ -375,7 +447,7 @@ def _make_mutable_class_init_fn(fields: Sequence[_Field]) -> Callable[..., None]
375
447
  " = ",
376
448
  attribute,
377
449
  )
378
- builder.append_ln("_self._unrecognized = ()")
450
+ builder.append_ln("_self._unrecognized = None")
379
451
  return make_function(
380
452
  name="__init__",
381
453
  params=params,
@@ -524,26 +596,19 @@ def _make_to_frozen_fn(
524
596
  builder.append_ln("ret._unrecognized = self._unrecognized")
525
597
 
526
598
  def array_len_expr() -> Expr:
527
- spans: list[LineSpanLike] = []
528
- spans.append(
529
- LineSpan.join(
530
- f"{_get_num_slots(fields)} + ",
531
- Expr.local("_len", len),
532
- "(ret._unrecognized) if ret._unrecognized else ",
533
- )
534
- )
599
+ exprs: list[ExprLike] = []
535
600
  # Fields are sorted by number.
536
601
  for field in reversed(fields):
537
602
  attr_expr = f"ret.{field.field.attribute}"
538
- spans.append(
603
+ exprs.append(
539
604
  LineSpan.join(
540
605
  f"{field.field.number + 1} if ",
541
606
  field.type.is_not_default_expr(attr_expr, attr_expr),
542
607
  " else ",
543
608
  )
544
609
  )
545
- spans.append("0")
546
- return Expr.join(*spans)
610
+ exprs.append("0")
611
+ return Expr.join(*exprs)
547
612
 
548
613
  # Set the _unrecognized field.
549
614
  builder.append_ln("ret._unrecognized = self._unrecognized")
@@ -575,8 +640,14 @@ def _make_eq_fn(
575
640
  builder.append_ln("if other.__class__ is self.__class__:")
576
641
  operands: list[ExprLike]
577
642
  if fields:
578
- attr: Callable[[_Field], str] = lambda f: f.field.attribute
579
- 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
+ ]
580
651
  else:
581
652
  operands = ["True"]
582
653
  builder.append_ln(" return ", Expr.join(*operands, separator=" and "))
@@ -639,9 +710,13 @@ def _make_repr_fn(fields: Sequence[_Field]) -> Callable[[Any], str]:
639
710
  )
640
711
 
641
712
 
642
- 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]:
643
716
  builder = BodyBuilder()
644
- 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
+ )
645
720
  builder.append_ln("ret = [0] * l")
646
721
  for field in fields:
647
722
  builder.append_ln(f"if l <= {field.field.number}:")
@@ -653,10 +728,8 @@ def _make_to_dense_json_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
653
728
  readable=False,
654
729
  ),
655
730
  )
656
- num_slots = _get_num_slots(fields)
657
- builder.append_ln(f"if l <= {num_slots}:")
658
- builder.append_ln(" return ret")
659
- 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")
660
733
  builder.append_ln("return ret")
661
734
  return make_function(
662
735
  name="to_dense_json",
@@ -690,11 +763,60 @@ def _make_to_readable_json_fn(fields: Sequence[_Field]) -> Callable[[Any], Any]:
690
763
  )
691
764
 
692
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
+
693
814
  def _make_from_json_fn(
694
815
  frozen_class: type,
695
816
  simple_class: type,
696
817
  fields: Sequence[_Field],
697
818
  removed_numbers: tuple[int, ...],
819
+ num_slots_incl_removed: int,
698
820
  ) -> Callable[[Any], Any]:
699
821
  builder = BodyBuilder()
700
822
  builder.append_ln("if not json:")
@@ -714,29 +836,39 @@ def _make_from_json_fn(
714
836
  "(json)",
715
837
  )
716
838
  for field in fields:
717
- name = field.field.name
718
839
  number = field.field.number
719
840
  item_expr = f"json[{number}]"
720
841
  builder.append_ln(
721
842
  f" ret.{field.field.attribute} = ",
722
843
  field.type.default_expr(),
723
844
  f" if array_len <= {number} else ",
724
- field.type.from_json_expr(item_expr),
845
+ field.type.from_json_expr(item_expr, "keep_unrecognized_fields"),
725
846
  )
726
- num_slots = _get_num_slots(fields)
727
- builder.append_ln(f" if array_len <= {num_slots}:")
728
- 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")
729
855
  builder.append_ln(
730
856
  " ret._array_len = ",
731
- _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
+ ),
732
862
  )
733
863
  builder.append_ln(" else:")
734
864
  builder.append_ln(
735
865
  " ret._unrecognized = ",
866
+ Expr.local("UnrecognizedFields", _UnrecognizedFields.from_json),
867
+ "(json=",
736
868
  Expr.local("deepcopy", copy.deepcopy),
737
- f"(json[{num_slots}:])",
869
+ f"(json[{num_slots_incl_removed}:]), adjusted_json_array_len=array_len)",
738
870
  )
739
- builder.append_ln(" ret._array_len = array_len")
871
+ builder.append_ln(f" ret._array_len = {num_slots_excl_removed}")
740
872
 
741
873
  builder.append_ln("else:")
742
874
  builder.append_ln(" array_len = 0")
@@ -748,12 +880,12 @@ def _make_from_json_fn(
748
880
  builder.append_ln(f" array_len = {field.field.number + 1}")
749
881
  builder.append_ln(
750
882
  f" {lvalue} = ",
751
- field.type.from_json_expr(f'json["{name}"]'),
883
+ field.type.from_json_expr(f'json["{name}"]', "keep_unrecognized_fields"),
752
884
  )
753
885
  builder.append_ln(" else:")
754
886
  builder.append_ln(f" {lvalue} = ", field.type.default_expr())
755
887
  # Drop unrecognized fields in readable mode.
756
- builder.append_ln(" ret._unrecognized = ()")
888
+ builder.append_ln(" ret._unrecognized = None")
757
889
  builder.append_ln(" ret._array_len = array_len")
758
890
 
759
891
  builder.append_ln("ret.__class__ = ", Expr.local("Frozen", frozen_class))
@@ -761,60 +893,121 @@ def _make_from_json_fn(
761
893
 
762
894
  return make_function(
763
895
  name="from_json",
764
- params=["json"],
896
+ params=["json", "keep_unrecognized_fields"],
897
+ body=builder.build(),
898
+ )
899
+
900
+
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(Expr.local(" decode_unused", decode_unused), "(stream)")
953
+ builder.append_ln(" start_offset = stream.position")
954
+ builder.append_ln(" for _ in range(array_len - {num_slots_incl_removed}):")
955
+ builder.append_ln(" ", Expr.local("decode_unused", decode_unused), "(stream)")
956
+ builder.append_ln(" end_offset = stream.position")
957
+ builder.append_ln(
958
+ " ret._unrecognized = ",
959
+ Expr.local("UnrecognizedFields", _UnrecognizedFields.from_bytes),
960
+ "(raw_bytes=stream.buffer[start_offset:end_offset], adjusted_bytes_array_len=array_len)",
961
+ )
962
+ builder.append_ln(" else:")
963
+ builder.append_ln(f" for _ in range(array_len - {num_slots_excl_removed}):")
964
+ builder.append_ln(" ", Expr.local("decode_unused", decode_unused), "(stream)")
965
+ builder.append_ln(" ret._unrecognized = None")
966
+ builder.append_ln("else:")
967
+ builder.append_ln(" ret._unrecognized = None")
968
+
969
+ builder.append_ln(
970
+ " ret._array_len = ",
971
+ _adjust_array_len_expr(
972
+ "array_len",
973
+ removed_numbers=removed_numbers,
974
+ num_slots_excl_removed=num_slots_excl_removed,
975
+ ),
976
+ )
977
+
978
+ builder.append_ln(
979
+ "ret.__class__ = ",
980
+ Expr.local("Frozen", frozen_class),
981
+ )
982
+ builder.append_ln("return ret")
983
+ return make_function(
984
+ name="decode",
985
+ params=["stream"],
765
986
  body=builder.build(),
766
987
  )
767
988
 
768
989
 
769
- def _adjust_array_len_expr(var: str, removed_numbers: tuple[int, ...]) -> str:
990
+ def _adjust_array_len_expr(
991
+ var: str,
992
+ removed_numbers: tuple[int, ...],
993
+ num_slots_excl_removed: int,
994
+ ) -> str:
770
995
  """
771
996
  When parsing a dense JSON or decoding a binary string, we can reuse the array length
772
997
  in the decoded struct, but we need to account for possibly newly-removed fields. The
773
998
  last field of the adjusted array length cannot be a removed field.
774
-
775
- Let's imagine that field number 3 was removed from a struct.
776
- This function would return the following expression:
777
- array_len if array_len <= 3 else 3 if array_len == 4 else array_len
778
999
  """
779
-
780
- @dataclass
781
- class _RemovedSpan:
782
- """Sequence of consecutive removed fields."""
783
-
784
- # Number of the first removed field.
785
- begin: int = 0
786
- # Number after the last removed field.
787
- end: int = 0
788
-
789
- def get_removed_spans() -> list[_RemovedSpan]:
790
- ret: list[_RemovedSpan] = []
791
- for number in sorted(removed_numbers):
792
- last = ret[-1] if ret else None
793
- if last and last.end == number:
794
- last.end = number + 1
795
- else:
796
- ret.append(_RemovedSpan(number, number + 1))
797
- return ret
798
-
799
- removed_spans = get_removed_spans()
800
-
801
- ret = ""
802
- lower_bound = 0
803
- for s in removed_spans:
804
- if s.begin == lower_bound:
805
- ret += f"{s.begin} if {var} <= {s.end} else "
806
- elif s.end == s.begin + 1:
807
- # Similar to the expression in 'else' but uses '==' instead of '<='
808
- ret += (
809
- f"{var} if {var} <= {s.begin} else {s.begin} if {var} == {s.end} else "
810
- )
1000
+ removed_numbers_set = set(removed_numbers)
1001
+ table: list[int] = [0]
1002
+ last_len = 0
1003
+ for i in range(0, num_slots_excl_removed - 1):
1004
+ if i in removed_numbers_set:
1005
+ table.append(last_len)
811
1006
  else:
812
- ret += (
813
- f"{var} if {var} <= {s.begin} else {s.begin} if {var} <= {s.end} else "
814
- )
815
- lower_bound = s.end + 1
816
- ret += var
817
- return ret
1007
+ table.append(i + 1)
1008
+ last_len = i + 1
1009
+ table_tuple = tuple(table)
1010
+ return f"{table_tuple}[{var}] if {var} < {num_slots_excl_removed} else {num_slots_excl_removed}"
818
1011
 
819
1012
 
820
1013
  def _init_default(default: Any, fields: Sequence[_Field]) -> None:
@@ -826,7 +1019,7 @@ def _init_default(default: Any, fields: Sequence[_Field]) -> None:
826
1019
  body=(Line.join("return ", field.type.default_expr()),),
827
1020
  )
828
1021
  object.__setattr__(default, attribute, get_field_default())
829
- object.__setattr__(default, "_unrecognized", ())
1022
+ object.__setattr__(default, "_unrecognized", None)
830
1023
  object.__setattr__(default, "_array_len", 0)
831
1024
 
832
1025
 
@@ -874,5 +1067,38 @@ def _name_private_is_frozen_attr(record_id: str) -> str:
874
1067
  return f"_is_{record_name}_{hex_hash}"
875
1068
 
876
1069
 
877
- def _get_num_slots(fields: Sequence[_Field]) -> int:
878
- return (fields[-1].field.number + 1) if fields else 0
1070
+ @dataclass(frozen=True)
1071
+ class _UnrecognizedFields:
1072
+ __slots__ = (
1073
+ "json",
1074
+ "raw_bytes",
1075
+ "adjusted_json_array_len",
1076
+ "adjusted_bytes_array_len",
1077
+ )
1078
+
1079
+ json: list[Any] | None
1080
+ raw_bytes: bytes | None
1081
+ adjusted_json_array_len: int | None
1082
+ adjusted_bytes_array_len: int | None
1083
+
1084
+ @staticmethod
1085
+ def from_json(
1086
+ json: list[Any], adjusted_json_array_len: int
1087
+ ) -> "_UnrecognizedFields":
1088
+ return _UnrecognizedFields(
1089
+ json=json,
1090
+ raw_bytes=None,
1091
+ adjusted_json_array_len=adjusted_json_array_len,
1092
+ adjusted_bytes_array_len=None,
1093
+ )
1094
+
1095
+ @staticmethod
1096
+ def from_bytes(
1097
+ raw_bytes: bytes, adjusted_bytes_array_len: int
1098
+ ) -> "_UnrecognizedFields":
1099
+ return _UnrecognizedFields(
1100
+ json=None,
1101
+ raw_bytes=raw_bytes,
1102
+ adjusted_json_array_len=None,
1103
+ adjusted_bytes_array_len=adjusted_bytes_array_len,
1104
+ )
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