soia-client 1.0.29__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (34) hide show
  1. {soia_client-1.0.29 → soia_client-1.1.0}/PKG-INFO +1 -1
  2. {soia_client-1.0.29 → soia_client-1.1.0}/pyproject.toml +1 -1
  3. {soia_client-1.0.29 → soia_client-1.1.0}/soia/__init__.py +3 -0
  4. soia_client-1.1.0/soia/_impl/keep.py +20 -0
  5. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/primitives.py +8 -4
  6. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/structs.py +90 -21
  7. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/timestamp.py +41 -4
  8. {soia_client-1.0.29 → soia_client-1.1.0}/soia_client.egg-info/PKG-INFO +1 -1
  9. {soia_client-1.0.29 → soia_client-1.1.0}/soia_client.egg-info/SOURCES.txt +1 -0
  10. {soia_client-1.0.29 → soia_client-1.1.0}/tests/test_module_initializer.py +87 -41
  11. {soia_client-1.0.29 → soia_client-1.1.0}/tests/test_timestamp.py +20 -2
  12. {soia_client-1.0.29 → soia_client-1.1.0}/LICENSE +0 -0
  13. {soia_client-1.0.29 → soia_client-1.1.0}/README.md +0 -0
  14. {soia_client-1.0.29 → soia_client-1.1.0}/setup.cfg +0 -0
  15. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/__init__.py +0 -0
  16. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/arrays.py +0 -0
  17. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/enums.py +0 -0
  18. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/function_maker.py +0 -0
  19. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/keyed_items.py +0 -0
  20. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/method.py +0 -0
  21. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/never.py +0 -0
  22. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/optionals.py +0 -0
  23. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/repr.py +0 -0
  24. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/serializer.py +0 -0
  25. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/serializers.py +0 -0
  26. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/service.py +0 -0
  27. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/service_client.py +0 -0
  28. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_impl/type_adapter.py +0 -0
  29. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_module_initializer.py +0 -0
  30. {soia_client-1.0.29 → soia_client-1.1.0}/soia/_spec.py +0 -0
  31. {soia_client-1.0.29 → soia_client-1.1.0}/soia/reflection.py +0 -0
  32. {soia_client-1.0.29 → soia_client-1.1.0}/soia_client.egg-info/dependency_links.txt +0 -0
  33. {soia_client-1.0.29 → soia_client-1.1.0}/soia_client.egg-info/top_level.txt +0 -0
  34. {soia_client-1.0.29 → soia_client-1.1.0}/tests/test_serializers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soia-client
3
- Version: 1.0.29
3
+ Version: 1.1.0
4
4
  Author-email: Tyler Fibonacci <gepheum@gmail.com>
5
5
  License: MIT License
6
6
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "soia-client"
7
- version = "1.0.29"
7
+ version = "1.1.0"
8
8
  description = ""
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Tyler Fibonacci", email = "gepheum@gmail.com" }]
@@ -1,5 +1,6 @@
1
1
  import typing as _typing
2
2
 
3
+ from soia._impl.keep import KEEP, Keep
3
4
  from soia._impl.keyed_items import KeyedItems
4
5
  from soia._impl.method import Method
5
6
  from soia._impl.serializer import Serializer
@@ -16,6 +17,8 @@ _: _typing.Final[_typing.Any] = None
16
17
 
17
18
  __all__ = [
18
19
  "_",
20
+ "Keep",
21
+ "KEEP",
19
22
  "KeyedItems",
20
23
  "Method",
21
24
  "RawServiceResponse",
@@ -0,0 +1,20 @@
1
+ from dataclasses import dataclass
2
+ from typing import Final, cast, final
3
+
4
+ from soia._impl.never import Never
5
+
6
+
7
+ @final
8
+ @dataclass(frozen=True)
9
+ class Keep:
10
+ """
11
+ Type of the KEEP constant, which indicates that a value should not be replaced.
12
+
13
+ Do not instantiate.
14
+ """
15
+
16
+ def __init__(self, never: Never):
17
+ pass
18
+
19
+
20
+ KEEP: Final = Keep(cast(Never, None))
@@ -1,3 +1,4 @@
1
+ import base64
1
2
  from collections.abc import Callable
2
3
  from dataclasses import dataclass
3
4
  from typing import Any, Final, final
@@ -280,8 +281,6 @@ STRING_ADAPTER: Final[TypeAdapter] = _StringAdapter()
280
281
 
281
282
 
282
283
  class _BytesAdapter(AbstractPrimitiveAdapter):
283
- _fromhex_fn: Final = bytes.fromhex
284
-
285
284
  def default_expr(self) -> ExprLike:
286
285
  return 'b""'
287
286
 
@@ -296,11 +295,16 @@ class _BytesAdapter(AbstractPrimitiveAdapter):
296
295
  in_expr: ExprLike,
297
296
  readable: bool,
298
297
  ) -> Expr:
299
- return Expr.join(in_expr, ".hex()")
298
+ return Expr.join(
299
+ Expr.local("b64encode", base64.b64encode),
300
+ "(",
301
+ in_expr,
302
+ ").decode('utf-8')",
303
+ )
300
304
 
301
305
  def from_json_expr(self, json_expr: ExprLike) -> Expr:
302
306
  return Expr.join(
303
- Expr.local("fromhex", _BytesAdapter._fromhex_fn), "(", json_expr, ' or "")'
307
+ Expr.local("b64decode", base64.b64decode), "(", json_expr, ' or "")'
304
308
  )
305
309
 
306
310
  def get_type(self) -> reflection.Type:
@@ -14,6 +14,7 @@ from soia._impl.function_maker import (
14
14
  Params,
15
15
  make_function,
16
16
  )
17
+ from soia._impl.keep import KEEP
17
18
  from soia._impl.repr import repr_impl
18
19
  from soia._impl.type_adapter import TypeAdapter
19
20
 
@@ -134,8 +135,12 @@ class StructAdapter(TypeAdapter):
134
135
  simple_class=simple_class,
135
136
  ),
136
137
  )
137
- mutable_class.__init__ = cast(Any, _make_mutable_class_init_fn(fields))
138
- frozen_class.whole = _make_whole_static_factory_method(frozen_class)
138
+ mutable_class.__init__ = _make_mutable_class_init_fn(fields)
139
+ frozen_class.partial = _make_partial_static_factory_method(
140
+ fields,
141
+ frozen_class,
142
+ )
143
+ frozen_class.replace = _make_replace_method(fields, frozen_class)
139
144
 
140
145
  frozen_class.__eq__ = _make_eq_fn(fields)
141
146
  frozen_class.__hash__ = cast(Any, _make_hash_fn(fields, self.record_hash))
@@ -270,13 +275,7 @@ def _make_frozen_class_init_fn(
270
275
  params: Params = ["_self"]
271
276
  if fields:
272
277
  params.append("*")
273
- for field in fields:
274
- params.append(
275
- Param(
276
- name=field.field.attribute,
277
- default=field.type.default_expr(),
278
- )
279
- )
278
+ params.extend(field.field.attribute for field in fields)
280
279
 
281
280
  builder = BodyBuilder()
282
281
  # Since __setattr__() was overridden to raise errors in order to make the class
@@ -384,10 +383,85 @@ def _make_mutable_class_init_fn(fields: Sequence[_Field]) -> Callable[..., None]
384
383
  )
385
384
 
386
385
 
387
- def _make_whole_static_factory_method(frozen_class: type) -> Callable[..., Any]:
388
- def whole(**kwargs):
389
- return frozen_class(**kwargs)
390
- return whole
386
+ def _make_partial_static_factory_method(
387
+ fields: Sequence[_Field],
388
+ frozen_class: type,
389
+ ) -> Callable[..., None]:
390
+ """
391
+ Returns the implementation of the partial() method of the frozen class.
392
+ """
393
+
394
+ params: Params = []
395
+ if fields:
396
+ params.append("*")
397
+ params.extend(
398
+ Param(
399
+ name=field.field.attribute,
400
+ default=field.type.default_expr(),
401
+ )
402
+ for field in fields
403
+ )
404
+
405
+ builder = BodyBuilder()
406
+ builder.append_ln(
407
+ "return ",
408
+ Expr.local("Frozen", frozen_class),
409
+ "(",
410
+ ", ".join(
411
+ f"{field.field.attribute}={field.field.attribute}" for field in fields
412
+ ),
413
+ ")",
414
+ )
415
+
416
+ return make_function(
417
+ name="partial",
418
+ params=params,
419
+ body=builder.build(),
420
+ )
421
+
422
+
423
+ def _make_replace_method(
424
+ fields: Sequence[_Field],
425
+ frozen_class: type,
426
+ ) -> Callable[..., None]:
427
+ """
428
+ Returns the implementation of the replace() method of the frozen class.
429
+ """
430
+
431
+ keep_local = Expr.local("KEEP", KEEP)
432
+ params: Params = []
433
+ if fields:
434
+ params.append("*")
435
+ params.extend(
436
+ Param(
437
+ name=field.field.attribute,
438
+ default=keep_local,
439
+ )
440
+ for field in fields
441
+ )
442
+
443
+ def field_to_arg_assigment(attr: str) -> LineSpan:
444
+ return LineSpan.join(
445
+ f"{attr}=self.{attr} if {attr} is ", keep_local, " else {attr}"
446
+ )
447
+
448
+ builder = BodyBuilder()
449
+ builder.append_ln(
450
+ "return ",
451
+ Expr.local("Frozen", frozen_class),
452
+ "(",
453
+ LineSpan.join(
454
+ *(field_to_arg_assigment(field.field.attribute) for field in fields),
455
+ separator=", ",
456
+ ),
457
+ ")",
458
+ )
459
+
460
+ return make_function(
461
+ name="partial",
462
+ params=params,
463
+ body=builder.build(),
464
+ )
391
465
 
392
466
 
393
467
  def _make_to_mutable_fn(
@@ -549,14 +623,9 @@ def _make_repr_fn(fields: Sequence[_Field]) -> Callable[[Any], str]:
549
623
  for field in fields:
550
624
  attribute = field.field.attribute
551
625
  # is_not_default_expr only works on a frozen expression.
552
- to_frozen_expr = field.type.to_frozen_expr(f"(self.{attribute})")
553
- is_not_default_expr = field.type.is_not_default_expr(
554
- to_frozen_expr, to_frozen_expr
555
- )
556
- builder.append_ln("if is_mutable or ", is_not_default_expr, ":")
557
- builder.append_ln(" r = ", repr_local, f"(self.{attribute})")
558
- builder.append_ln(f" assignments.append(f'{attribute}={{r.indented}}')")
559
- builder.append_ln(" any_complex = any_complex or r.complex")
626
+ builder.append_ln("r = ", repr_local, f"(self.{attribute})")
627
+ builder.append_ln(f"assignments.append(f'{attribute}={{r.indented}}')")
628
+ builder.append_ln("any_complex = any_complex or r.complex")
560
629
  builder.append_ln("if len(assignments) <= 1 and not any_complex:")
561
630
  builder.append_ln(" body = ''.join(assignments)")
562
631
  builder.append_ln("else:")
@@ -5,6 +5,14 @@ from typing import Any, Final, Union, cast, final, overload
5
5
 
6
6
  @final
7
7
  class Timestamp:
8
+ """
9
+ A number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z).
10
+
11
+ Does not contain any timezone information.
12
+ Convertible to and from datetime objects.
13
+ Immutable.
14
+ """
15
+
8
16
  __slots__ = ("unix_millis",)
9
17
 
10
18
  unix_millis: int
@@ -26,7 +34,14 @@ class Timestamp:
26
34
 
27
35
  @staticmethod
28
36
  def from_datetime(dt: datetime.datetime) -> "Timestamp":
29
- return Timestamp.from_unix_seconds(dt.timestamp())
37
+ # dt.timestamp() mail fail if the year is not in [1970, 2038]
38
+ if dt.tzinfo is None:
39
+ timestamp = (
40
+ dt - _EPOCH_DT.astimezone().replace(tzinfo=None)
41
+ ).total_seconds()
42
+ else:
43
+ timestamp = (dt - _EPOCH_DT).total_seconds()
44
+ return Timestamp.from_unix_seconds(timestamp)
30
45
 
31
46
  @staticmethod
32
47
  def now() -> "Timestamp":
@@ -41,9 +56,26 @@ class Timestamp:
41
56
  return self.unix_millis / 1000.0
42
57
 
43
58
  def to_datetime_or_raise(self) -> datetime.datetime:
44
- return datetime.datetime.fromtimestamp(
45
- self.unix_seconds, tz=datetime.timezone.utc
46
- )
59
+ """
60
+ Returns a datetime object representing the timestamp in UTC timezone.
61
+
62
+ Raises an exception if the timestamp is out of bounds for datetime.
63
+ If you don't want the exception, use 'to_datetime_or_limit()' instead.
64
+ """
65
+ return _EPOCH_DT + datetime.timedelta(seconds=self.unix_seconds)
66
+
67
+ def to_datetime_or_limit(self) -> datetime.datetime:
68
+ """
69
+ Returns a datetime object representing the timestamp in UTC timezone.
70
+
71
+ Clamps the timestamp to the minimum or maximum datetime if it is out of bounds.
72
+ """
73
+ if self.unix_seconds <= (_MIN_DT_UTC - _EPOCH_DT).total_seconds():
74
+ return datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)
75
+ elif self.unix_seconds >= (_MAX_DT_UTC - _EPOCH_DT).total_seconds():
76
+ return datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)
77
+ else:
78
+ return self.to_datetime_or_raise()
47
79
 
48
80
  def __add__(self, td: datetime.timedelta) -> "Timestamp":
49
81
  return Timestamp(
@@ -126,3 +158,8 @@ class Timestamp:
126
158
  setattr(Timestamp, "EPOCH", Timestamp.from_unix_millis(0))
127
159
  setattr(Timestamp, "MIN", Timestamp.from_unix_millis(-8640000000000000))
128
160
  setattr(Timestamp, "MAX", Timestamp.from_unix_millis(8640000000000000))
161
+
162
+
163
+ _EPOCH_DT: Final = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
164
+ _MIN_DT_UTC: Final = datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)
165
+ _MAX_DT_UTC: Final = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soia-client
3
- Version: 1.0.29
3
+ Version: 1.1.0
4
4
  Author-email: Tyler Fibonacci <gepheum@gmail.com>
5
5
  License: MIT License
6
6
 
@@ -9,6 +9,7 @@ soia/_impl/__init__.py
9
9
  soia/_impl/arrays.py
10
10
  soia/_impl/enums.py
11
11
  soia/_impl/function_maker.py
12
+ soia/_impl/keep.py
12
13
  soia/_impl/keyed_items.py
13
14
  soia/_impl/method.py
14
15
  soia/_impl/never.py
@@ -363,11 +363,11 @@ class ModuleInitializerTestCase(unittest.TestCase):
363
363
  self.assertEqual(point.x, 1.5)
364
364
  self.assertEqual(point.y, 2.5)
365
365
 
366
- def test_whole_static_factory_method(self):
366
+ def test_partial_static_factory_method(self):
367
367
  point_cls = self.init_test_module()["Point"]
368
- point = point_cls.whole(x=1.5, y=2.5)
368
+ point = point_cls.partial(x=1.5)
369
369
  self.assertEqual(point.x, 1.5)
370
- self.assertEqual(point.y, 2.5)
370
+ self.assertEqual(point.y, 0.0)
371
371
 
372
372
  def test_to_mutable(self):
373
373
  point_cls = self.init_test_module()["Point"]
@@ -388,7 +388,8 @@ class ModuleInitializerTestCase(unittest.TestCase):
388
388
  self.assertEqual(hash(a), hash(b))
389
389
  self.assertNotEqual(a, c)
390
390
  self.assertNotEqual(a, "foo")
391
- self.assertEqual(point_cls(), point_cls(x=0.0, y=0.0))
391
+ self.assertEqual(point_cls.partial(), point_cls(x=0.0, y=0.0))
392
+ self.assertEqual(point_cls.DEFAULT, point_cls(x=0.0, y=0.0))
392
393
 
393
394
  def test_or_mutable(self):
394
395
  point_cls = self.init_test_module()["Point"]
@@ -405,8 +406,9 @@ class ModuleInitializerTestCase(unittest.TestCase):
405
406
  i64=0,
406
407
  u64=0,
407
408
  t=Timestamp.EPOCH,
409
+ s="",
408
410
  )
409
- b = primitives_cls()
411
+ b = primitives_cls.partial()
410
412
  self.assertEqual(a, b)
411
413
  self.assertEqual(hash(a), hash(b))
412
414
 
@@ -422,8 +424,9 @@ class ModuleInitializerTestCase(unittest.TestCase):
422
424
  i64=2,
423
425
  u64=3,
424
426
  t=Timestamp.from_unix_millis(4),
427
+ s="",
425
428
  )
426
- self.assertEqual(serializer.to_json(p), [1, "61", 3.14, 3.14, 1, 2, 3, "", 4])
429
+ self.assertEqual(serializer.to_json(p), [1, "YQ==", 3.14, 3.14, 1, 2, 3, "", 4])
427
430
 
428
431
  def test_primitives_from_json(self):
429
432
  primitives_cls = self.init_test_module()["Primitives"]
@@ -431,7 +434,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
431
434
  json = [0] * 100
432
435
  self.assertEqual(serializer.from_json(json), primitives_cls.DEFAULT)
433
436
  self.assertEqual(
434
- serializer.from_json([1, "61", 3.14, 3.14, 1, 2, 3, "", 4]),
437
+ serializer.from_json([1, "YQ==", 3.14, 3.14, 1, 2, 3, "", 4]),
435
438
  primitives_cls(
436
439
  bool=True,
437
440
  bytes=b"a",
@@ -441,6 +444,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
441
444
  i64=2,
442
445
  u64=3,
443
446
  t=Timestamp.from_unix_millis(4),
447
+ s="",
444
448
  ),
445
449
  )
446
450
 
@@ -455,10 +459,11 @@ class ModuleInitializerTestCase(unittest.TestCase):
455
459
  i64=2,
456
460
  u64=3,
457
461
  t=Timestamp.from_unix_millis(4),
462
+ s="",
458
463
  )
459
464
  self.assertEqual(
460
465
  str(p),
461
- "Primitives(\n bool=True,\n bytes=b'a',\n f32=3.14,\n f64=3.14,\n i32=1,\n i64=2,\n u64=3,\n t=Timestamp(\n unix_millis=4,\n _formatted='1970-01-01T00:00:00.004000Z',\n ),\n)",
466
+ "Primitives(\n bool=True,\n bytes=b'a',\n f32=3.14,\n f64=3.14,\n i32=1,\n i64=2,\n u64=3,\n s='',\n t=Timestamp(\n unix_millis=4,\n _formatted='1970-01-01T00:00:00.004000Z',\n ),\n)",
462
467
  )
463
468
 
464
469
  def test_from_json_converts_between_ints_and_floats(self):
@@ -503,7 +508,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
503
508
  point_cls = self.init_test_module()["Point"]
504
509
  serializer = point_cls.SERIALIZER
505
510
  self.assertEqual(serializer.from_json([1.5, 0, 2.5]), point_cls(x=1.5, y=2.5))
506
- self.assertEqual(serializer.from_json([1.5]), point_cls(x=1.5))
511
+ self.assertEqual(serializer.from_json([1.5]), point_cls(x=1.5, y=0.0))
507
512
  self.assertEqual(serializer.from_json([0.0]), point_cls.DEFAULT)
508
513
  self.assertEqual(serializer.from_json(0), point_cls.DEFAULT)
509
514
 
@@ -534,28 +539,30 @@ class ModuleInitializerTestCase(unittest.TestCase):
534
539
  foobar_cls = test_module["Foobar"]
535
540
  point_cls = test_module["Point"]
536
541
  serializer = foobar_cls.SERIALIZER
537
- foobar = foobar_cls()
542
+ foobar = foobar_cls.partial()
538
543
  self.assertEqual(serializer.to_json_code(foobar), "[]")
539
544
  self.assertEqual(serializer.from_json_code("[]"), foobar)
540
- foobar = foobar_cls(a=3)
545
+ foobar = foobar_cls.partial(a=3)
541
546
  self.assertEqual(serializer.to_json_code(foobar), "[0,3]")
542
547
  self.assertEqual(serializer.from_json_code("[0,3]"), foobar)
543
548
  self.assertEqual(serializer.from_json_code("[0,3.1]"), foobar)
544
- foobar = foobar_cls(b=3, point=point_cls.DEFAULT)
549
+ foobar = foobar_cls(a=0, b=3, point=point_cls.DEFAULT)
545
550
  self.assertEqual(serializer.to_json_code(foobar), "[0,0,0,3]")
546
551
  self.assertEqual(serializer.from_json_code("[0,0,0,3]"), foobar)
547
552
  self.assertEqual(serializer.from_json_code("[0,0,0,3.1]"), foobar)
548
- foobar = foobar_cls(point=point_cls(x=2))
553
+ foobar = foobar_cls.partial(point=point_cls.partial(x=2))
549
554
  self.assertEqual(serializer.to_json_code(foobar), "[0,0,0,0,[2.0]]")
550
555
  self.assertEqual(serializer.from_json_code("[0,0,0,0,[2.0]]"), foobar)
551
556
 
552
557
  def test_recursive_struct(self):
553
558
  rec_cls = self.init_test_module()["Rec"]
554
- r = rec_cls(r=rec_cls(r=rec_cls.DEFAULT, x=1))
559
+ r = rec_cls.partial(r=rec_cls(r=rec_cls.DEFAULT, x=1))
555
560
  serializer = rec_cls.SERIALIZER
556
561
  self.assertEqual(serializer.to_json_code(r), "[[[],1]]")
557
562
  self.assertEqual(serializer.from_json_code("[[[],1]]"), r)
558
- self.assertEqual(str(r), "Rec(\n r=Rec(x=1),\n)")
563
+ self.assertEqual(
564
+ str(r), "Rec(\n r=Rec(\n r=Rec.DEFAULT,\n x=1,\n ),\n x=0,\n)"
565
+ )
559
566
 
560
567
  def test_struct_ctor_accepts_mutable_struct(self):
561
568
  module = self.init_test_module()
@@ -564,6 +571,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
564
571
  segment = segment_cls(
565
572
  a=point_cls(x=1.0, y=2.0).to_mutable(),
566
573
  b=point_cls(x=3.0, y=4.0),
574
+ c=None,
567
575
  )
568
576
  self.assertEqual(
569
577
  segment,
@@ -578,7 +586,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
578
586
  module = self.init_test_module()
579
587
  segment_cls = module["Segment"]
580
588
  try:
581
- segment_cls(
589
+ segment_cls.partial(
582
590
  # Should be a Point
583
591
  a=segment_cls.DEFAULT,
584
592
  )
@@ -588,9 +596,18 @@ class ModuleInitializerTestCase(unittest.TestCase):
588
596
 
589
597
  def test_struct_ctor_raises_error_if_unknown_arg(self):
590
598
  module = self.init_test_module()
591
- segment_cls = module["Segment"]
599
+ point_cls = module["Point"]
592
600
  try:
593
- segment_cls(foo=4)
601
+ point_cls(x=1, b=2, foo=4)
602
+ self.fail("Expected to fail")
603
+ except Exception:
604
+ pass
605
+
606
+ def test_struct_ctor_raises_error_if_missing_arg(self):
607
+ module = self.init_test_module()
608
+ point_cls = module["Point"]
609
+ try:
610
+ point_cls(x=1)
594
611
  self.fail("Expected to fail")
595
612
  except Exception:
596
613
  pass
@@ -655,13 +672,13 @@ class ModuleInitializerTestCase(unittest.TestCase):
655
672
  module = self.init_test_module()
656
673
  segment_cls = module["Segment"]
657
674
  point_cls = module["Point"]
658
- segment = segment_cls(
675
+ segment = segment_cls.partial(
659
676
  c=point_cls.Mutable(x=1.0, y=2.0),
660
677
  )
661
678
  other_segment = segment.to_mutable().to_frozen()
662
679
  self.assertEqual(
663
680
  other_segment,
664
- segment_cls(
681
+ segment_cls.partial(
665
682
  c=point_cls(x=1.0, y=2.0),
666
683
  ),
667
684
  )
@@ -711,7 +728,9 @@ class ModuleInitializerTestCase(unittest.TestCase):
711
728
  module = self.init_test_module()
712
729
  json_value_cls = module["JsonValue"]
713
730
  json_object_cls = json_value_cls.Object
714
- json_object = json_value_cls.wrap_object(json_object_cls().to_mutable())
731
+ json_object = json_value_cls.wrap_object(
732
+ json_object_cls(entries=[]).to_mutable()
733
+ )
715
734
  self.assertEqual(json_object.kind, "object")
716
735
  self.assertEqual(json_object.value, json_object_cls.DEFAULT)
717
736
  self.assertEqual(
@@ -780,7 +799,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
780
799
  json_value_cls.wrap_object(
781
800
  json_value_cls.Object(
782
801
  entries=[
783
- json_value_cls.ObjectEntry(),
802
+ json_value_cls.ObjectEntry.partial(),
784
803
  json_value_cls.ObjectEntry.DEFAULT,
785
804
  ],
786
805
  )
@@ -815,7 +834,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
815
834
  json_value_cls = self.init_test_module()["JsonValue"]
816
835
  json_object_entry_cls = json_value_cls.ObjectEntry
817
836
  self.assertEqual(json_object_entry_cls.DEFAULT.value, json_value_cls.UNKNOWN)
818
- self.assertEqual(json_object_entry_cls().value, json_value_cls.UNKNOWN)
837
+ self.assertEqual(json_object_entry_cls.partial().value, json_value_cls.UNKNOWN)
819
838
 
820
839
  def test_enum_with_unrecognized_and_removed_fields(self):
821
840
  json_value_cls = self.init_test_module()["JsonValue"]
@@ -849,11 +868,18 @@ class ModuleInitializerTestCase(unittest.TestCase):
849
868
  module = self.init_test_module()
850
869
  point_cls = module["Point"]
851
870
  self.assertEqual(
852
- repr(point_cls(x=1.5)),
853
- "Point(x=1.5)",
871
+ repr(point_cls.partial(x=1.5)),
872
+ "\n".join(
873
+ [
874
+ "Point(",
875
+ " x=1.5,",
876
+ " y=0.0,",
877
+ ")",
878
+ ]
879
+ ),
854
880
  )
855
881
  self.assertEqual(
856
- repr(point_cls(x=1.5).to_mutable()),
882
+ repr(point_cls.partial(x=1.5).to_mutable()),
857
883
  "\n".join(
858
884
  [
859
885
  "Point.Mutable(",
@@ -875,8 +901,15 @@ class ModuleInitializerTestCase(unittest.TestCase):
875
901
  ),
876
902
  )
877
903
  self.assertEqual(
878
- repr(point_cls()),
879
- "Point()",
904
+ repr(point_cls.partial()),
905
+ "\n".join(
906
+ [
907
+ "Point(",
908
+ " x=0.0,",
909
+ " y=0.0,",
910
+ ")",
911
+ ]
912
+ ),
880
913
  )
881
914
  self.assertEqual(
882
915
  repr(point_cls.DEFAULT),
@@ -896,19 +929,22 @@ class ModuleInitializerTestCase(unittest.TestCase):
896
929
  shape_cls = module["Shape"]
897
930
  self.assertEqual(
898
931
  repr(shape_cls(points=[])),
899
- "Shape()",
932
+ "Shape(points=[])",
900
933
  )
901
934
  self.assertEqual(
902
935
  repr(shape_cls(points=[]).to_mutable()),
903
936
  "Shape.Mutable(points=[])",
904
937
  )
905
938
  self.assertEqual(
906
- repr(shape_cls(points=[point_cls(x=1.5)])),
939
+ repr(shape_cls(points=[point_cls(x=1.5, y=0.0)])),
907
940
  "\n".join(
908
941
  [
909
942
  "Shape(",
910
943
  " points=[",
911
- " Point(x=1.5),",
944
+ " Point(",
945
+ " x=1.5,",
946
+ " y=0.0,",
947
+ " ),",
912
948
  " ],",
913
949
  ")",
914
950
  ]
@@ -918,8 +954,8 @@ class ModuleInitializerTestCase(unittest.TestCase):
918
954
  repr(
919
955
  shape_cls(
920
956
  points=[
921
- point_cls(x=1.5),
922
- point_cls(y=2.5),
957
+ point_cls.partial(x=1.5),
958
+ point_cls.partial(y=2.5),
923
959
  ],
924
960
  )
925
961
  ),
@@ -927,8 +963,14 @@ class ModuleInitializerTestCase(unittest.TestCase):
927
963
  [
928
964
  "Shape(",
929
965
  " points=[",
930
- " Point(x=1.5),",
931
- " Point(y=2.5),",
966
+ " Point(",
967
+ " x=1.5,",
968
+ " y=0.0,",
969
+ " ),",
970
+ " Point(",
971
+ " x=0.0,",
972
+ " y=2.5,",
973
+ " ),",
932
974
  " ],",
933
975
  ")",
934
976
  ]
@@ -938,8 +980,8 @@ class ModuleInitializerTestCase(unittest.TestCase):
938
980
  repr(
939
981
  shape_cls.Mutable(
940
982
  points=[
941
- point_cls(x=1.5),
942
- point_cls(y=2.5).to_mutable(),
983
+ point_cls.partial(x=1.5),
984
+ point_cls.partial(y=2.5).to_mutable(),
943
985
  ]
944
986
  )
945
987
  ),
@@ -947,7 +989,10 @@ class ModuleInitializerTestCase(unittest.TestCase):
947
989
  [
948
990
  "Shape.Mutable(",
949
991
  " points=[",
950
- " Point(x=1.5),",
992
+ " Point(",
993
+ " x=1.5,",
994
+ " y=0.0,",
995
+ " ),",
951
996
  " Point.Mutable(",
952
997
  " x=0.0,",
953
998
  " y=2.5,",
@@ -991,15 +1036,15 @@ class ModuleInitializerTestCase(unittest.TestCase):
991
1036
  ),
992
1037
  )
993
1038
  self.assertEqual(
994
- repr(json_value_cls.wrap_object(json_object_cls().DEFAULT)),
1039
+ repr(json_value_cls.wrap_object(json_object_cls.DEFAULT)),
995
1040
  "JsonValue.wrap_object(JsonValue.Object.DEFAULT)",
996
1041
  )
997
1042
  self.assertEqual(
998
- repr(json_value_cls.wrap_object(json_object_cls())),
1043
+ repr(json_value_cls.wrap_object(json_object_cls.partial())),
999
1044
  "\n".join(
1000
1045
  [
1001
1046
  "JsonValue.wrap_object(",
1002
- " JsonValue.Object()",
1047
+ " JsonValue.Object(entries=[])",
1003
1048
  ")",
1004
1049
  ]
1005
1050
  ),
@@ -1082,6 +1127,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
1082
1127
  segment = segment_cls(
1083
1128
  a=point_cls(x=1.0, y=2.0),
1084
1129
  b=point_cls(x=3.0, y=4.0),
1130
+ c=None,
1085
1131
  ).to_mutable()
1086
1132
  a = segment.mutable_a
1087
1133
  self.assertIsInstance(a, point_cls.Mutable)
@@ -2,6 +2,7 @@ import dataclasses
2
2
  import unittest
3
3
  from datetime import datetime, timedelta, timezone
4
4
  from typing import Any
5
+ from zoneinfo import ZoneInfo
5
6
 
6
7
  from soia import Timestamp
7
8
 
@@ -23,6 +24,14 @@ class TimestampTestCase(unittest.TestCase):
23
24
  def test_from_datetime(self):
24
25
  ts = Timestamp.from_datetime(datetime.fromtimestamp(200, tz=timezone.utc))
25
26
  self.assertEqual(ts.unix_millis, 200000)
27
+ ts = Timestamp.from_datetime(
28
+ datetime.fromtimestamp(200, tz=ZoneInfo("America/New_York"))
29
+ )
30
+ self.assertEqual(ts.unix_millis, 200000)
31
+ ts = Timestamp.from_datetime(datetime.fromtimestamp(200))
32
+ self.assertEqual(ts.unix_millis, 200000)
33
+ ts = Timestamp.from_datetime(datetime.min)
34
+ ts = Timestamp.from_datetime(datetime.max)
26
35
 
27
36
  def test_epoch(self):
28
37
  self.assertEqual(Timestamp.EPOCH.unix_millis, 0)
@@ -76,11 +85,12 @@ class TimestampTestCase(unittest.TestCase):
76
85
  pass
77
86
 
78
87
  def test_to_datetime(self):
79
- ts = Timestamp.from_unix_seconds(200)
80
88
  self.assertEqual(
81
- ts.to_datetime_or_raise(),
89
+ Timestamp.from_unix_seconds(200).to_datetime_or_raise(),
82
90
  datetime.fromtimestamp(200, tz=timezone.utc),
83
91
  )
92
+ Timestamp.from_datetime(datetime.min + timedelta(days=1)).to_datetime_or_raise()
93
+ Timestamp.from_datetime(datetime.max - timedelta(days=1)).to_datetime_or_raise()
84
94
 
85
95
  def test_to_datetime_out_of_bound(self):
86
96
  try:
@@ -88,6 +98,14 @@ class TimestampTestCase(unittest.TestCase):
88
98
  self.fail("Expected to fail with OverflowError or ValueError")
89
99
  except Exception:
90
100
  pass
101
+ self.assertEqual(
102
+ Timestamp.MIN.to_datetime_or_limit(),
103
+ datetime.min.replace(tzinfo=timezone.utc),
104
+ )
105
+ self.assertEqual(
106
+ Timestamp.MAX.to_datetime_or_limit(),
107
+ datetime.max.replace(tzinfo=timezone.utc),
108
+ )
91
109
 
92
110
  def test_add_timedelta(self):
93
111
  ts = Timestamp.from_unix_seconds(200)
File without changes
File without changes
File without changes
File without changes