soia-client 1.0.30__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.30 → soia_client-1.1.0}/PKG-INFO +1 -1
  2. {soia_client-1.0.30 → soia_client-1.1.0}/pyproject.toml +1 -1
  3. {soia_client-1.0.30 → 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.30 → soia_client-1.1.0}/soia/_impl/primitives.py +5 -2
  6. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/structs.py +90 -21
  7. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/timestamp.py +41 -4
  8. {soia_client-1.0.30 → soia_client-1.1.0}/soia_client.egg-info/PKG-INFO +1 -1
  9. {soia_client-1.0.30 → soia_client-1.1.0}/soia_client.egg-info/SOURCES.txt +1 -0
  10. {soia_client-1.0.30 → soia_client-1.1.0}/tests/test_module_initializer.py +87 -40
  11. {soia_client-1.0.30 → soia_client-1.1.0}/tests/test_timestamp.py +20 -2
  12. {soia_client-1.0.30 → soia_client-1.1.0}/LICENSE +0 -0
  13. {soia_client-1.0.30 → soia_client-1.1.0}/README.md +0 -0
  14. {soia_client-1.0.30 → soia_client-1.1.0}/setup.cfg +0 -0
  15. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/__init__.py +0 -0
  16. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/arrays.py +0 -0
  17. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/enums.py +0 -0
  18. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/function_maker.py +0 -0
  19. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/keyed_items.py +0 -0
  20. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/method.py +0 -0
  21. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/never.py +0 -0
  22. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/optionals.py +0 -0
  23. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/repr.py +0 -0
  24. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/serializer.py +0 -0
  25. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/serializers.py +0 -0
  26. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/service.py +0 -0
  27. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/service_client.py +0 -0
  28. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_impl/type_adapter.py +0 -0
  29. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_module_initializer.py +0 -0
  30. {soia_client-1.0.30 → soia_client-1.1.0}/soia/_spec.py +0 -0
  31. {soia_client-1.0.30 → soia_client-1.1.0}/soia/reflection.py +0 -0
  32. {soia_client-1.0.30 → soia_client-1.1.0}/soia_client.egg-info/dependency_links.txt +0 -0
  33. {soia_client-1.0.30 → soia_client-1.1.0}/soia_client.egg-info/top_level.txt +0 -0
  34. {soia_client-1.0.30 → 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.30
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.30"
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))
@@ -3,11 +3,12 @@ from collections.abc import Callable
3
3
  from dataclasses import dataclass
4
4
  from typing import Any, Final, final
5
5
 
6
- from soia import _spec, reflection
7
6
  from soia._impl.function_maker import Expr, ExprLike
8
7
  from soia._impl.timestamp import Timestamp
9
8
  from soia._impl.type_adapter import TypeAdapter
10
9
 
10
+ from soia import _spec, reflection
11
+
11
12
 
12
13
  class AbstractPrimitiveAdapter(TypeAdapter):
13
14
  @final
@@ -296,7 +297,9 @@ class _BytesAdapter(AbstractPrimitiveAdapter):
296
297
  ) -> Expr:
297
298
  return Expr.join(
298
299
  Expr.local("b64encode", base64.b64encode),
299
- "(", in_expr, ").decode('utf-8')",
300
+ "(",
301
+ in_expr,
302
+ ").decode('utf-8')",
300
303
  )
301
304
 
302
305
  def from_json_expr(self, json_expr: ExprLike) -> Expr:
@@ -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.30
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
@@ -2,10 +2,11 @@ import dataclasses
2
2
  import unittest
3
3
  from typing import Any
4
4
 
5
- from soia import KeyedItems, Method, Timestamp, _spec
6
5
  from soia._module_initializer import init_module
7
6
  from soia.reflection import TypeDescriptor
8
7
 
8
+ from soia import KeyedItems, Method, Timestamp, _spec
9
+
9
10
 
10
11
  class ModuleInitializerTestCase(unittest.TestCase):
11
12
  def init_test_module(self) -> dict[str, Any]:
@@ -362,11 +363,11 @@ class ModuleInitializerTestCase(unittest.TestCase):
362
363
  self.assertEqual(point.x, 1.5)
363
364
  self.assertEqual(point.y, 2.5)
364
365
 
365
- def test_whole_static_factory_method(self):
366
+ def test_partial_static_factory_method(self):
366
367
  point_cls = self.init_test_module()["Point"]
367
- point = point_cls.whole(x=1.5, y=2.5)
368
+ point = point_cls.partial(x=1.5)
368
369
  self.assertEqual(point.x, 1.5)
369
- self.assertEqual(point.y, 2.5)
370
+ self.assertEqual(point.y, 0.0)
370
371
 
371
372
  def test_to_mutable(self):
372
373
  point_cls = self.init_test_module()["Point"]
@@ -387,7 +388,8 @@ class ModuleInitializerTestCase(unittest.TestCase):
387
388
  self.assertEqual(hash(a), hash(b))
388
389
  self.assertNotEqual(a, c)
389
390
  self.assertNotEqual(a, "foo")
390
- 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))
391
393
 
392
394
  def test_or_mutable(self):
393
395
  point_cls = self.init_test_module()["Point"]
@@ -404,8 +406,9 @@ class ModuleInitializerTestCase(unittest.TestCase):
404
406
  i64=0,
405
407
  u64=0,
406
408
  t=Timestamp.EPOCH,
409
+ s="",
407
410
  )
408
- b = primitives_cls()
411
+ b = primitives_cls.partial()
409
412
  self.assertEqual(a, b)
410
413
  self.assertEqual(hash(a), hash(b))
411
414
 
@@ -421,6 +424,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
421
424
  i64=2,
422
425
  u64=3,
423
426
  t=Timestamp.from_unix_millis(4),
427
+ s="",
424
428
  )
425
429
  self.assertEqual(serializer.to_json(p), [1, "YQ==", 3.14, 3.14, 1, 2, 3, "", 4])
426
430
 
@@ -440,6 +444,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
440
444
  i64=2,
441
445
  u64=3,
442
446
  t=Timestamp.from_unix_millis(4),
447
+ s="",
443
448
  ),
444
449
  )
445
450
 
@@ -454,10 +459,11 @@ class ModuleInitializerTestCase(unittest.TestCase):
454
459
  i64=2,
455
460
  u64=3,
456
461
  t=Timestamp.from_unix_millis(4),
462
+ s="",
457
463
  )
458
464
  self.assertEqual(
459
465
  str(p),
460
- "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)",
461
467
  )
462
468
 
463
469
  def test_from_json_converts_between_ints_and_floats(self):
@@ -502,7 +508,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
502
508
  point_cls = self.init_test_module()["Point"]
503
509
  serializer = point_cls.SERIALIZER
504
510
  self.assertEqual(serializer.from_json([1.5, 0, 2.5]), point_cls(x=1.5, y=2.5))
505
- 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))
506
512
  self.assertEqual(serializer.from_json([0.0]), point_cls.DEFAULT)
507
513
  self.assertEqual(serializer.from_json(0), point_cls.DEFAULT)
508
514
 
@@ -533,28 +539,30 @@ class ModuleInitializerTestCase(unittest.TestCase):
533
539
  foobar_cls = test_module["Foobar"]
534
540
  point_cls = test_module["Point"]
535
541
  serializer = foobar_cls.SERIALIZER
536
- foobar = foobar_cls()
542
+ foobar = foobar_cls.partial()
537
543
  self.assertEqual(serializer.to_json_code(foobar), "[]")
538
544
  self.assertEqual(serializer.from_json_code("[]"), foobar)
539
- foobar = foobar_cls(a=3)
545
+ foobar = foobar_cls.partial(a=3)
540
546
  self.assertEqual(serializer.to_json_code(foobar), "[0,3]")
541
547
  self.assertEqual(serializer.from_json_code("[0,3]"), foobar)
542
548
  self.assertEqual(serializer.from_json_code("[0,3.1]"), foobar)
543
- foobar = foobar_cls(b=3, point=point_cls.DEFAULT)
549
+ foobar = foobar_cls(a=0, b=3, point=point_cls.DEFAULT)
544
550
  self.assertEqual(serializer.to_json_code(foobar), "[0,0,0,3]")
545
551
  self.assertEqual(serializer.from_json_code("[0,0,0,3]"), foobar)
546
552
  self.assertEqual(serializer.from_json_code("[0,0,0,3.1]"), foobar)
547
- foobar = foobar_cls(point=point_cls(x=2))
553
+ foobar = foobar_cls.partial(point=point_cls.partial(x=2))
548
554
  self.assertEqual(serializer.to_json_code(foobar), "[0,0,0,0,[2.0]]")
549
555
  self.assertEqual(serializer.from_json_code("[0,0,0,0,[2.0]]"), foobar)
550
556
 
551
557
  def test_recursive_struct(self):
552
558
  rec_cls = self.init_test_module()["Rec"]
553
- 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))
554
560
  serializer = rec_cls.SERIALIZER
555
561
  self.assertEqual(serializer.to_json_code(r), "[[[],1]]")
556
562
  self.assertEqual(serializer.from_json_code("[[[],1]]"), r)
557
- 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
+ )
558
566
 
559
567
  def test_struct_ctor_accepts_mutable_struct(self):
560
568
  module = self.init_test_module()
@@ -563,6 +571,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
563
571
  segment = segment_cls(
564
572
  a=point_cls(x=1.0, y=2.0).to_mutable(),
565
573
  b=point_cls(x=3.0, y=4.0),
574
+ c=None,
566
575
  )
567
576
  self.assertEqual(
568
577
  segment,
@@ -577,7 +586,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
577
586
  module = self.init_test_module()
578
587
  segment_cls = module["Segment"]
579
588
  try:
580
- segment_cls(
589
+ segment_cls.partial(
581
590
  # Should be a Point
582
591
  a=segment_cls.DEFAULT,
583
592
  )
@@ -587,9 +596,18 @@ class ModuleInitializerTestCase(unittest.TestCase):
587
596
 
588
597
  def test_struct_ctor_raises_error_if_unknown_arg(self):
589
598
  module = self.init_test_module()
590
- segment_cls = module["Segment"]
599
+ point_cls = module["Point"]
591
600
  try:
592
- 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)
593
611
  self.fail("Expected to fail")
594
612
  except Exception:
595
613
  pass
@@ -654,13 +672,13 @@ class ModuleInitializerTestCase(unittest.TestCase):
654
672
  module = self.init_test_module()
655
673
  segment_cls = module["Segment"]
656
674
  point_cls = module["Point"]
657
- segment = segment_cls(
675
+ segment = segment_cls.partial(
658
676
  c=point_cls.Mutable(x=1.0, y=2.0),
659
677
  )
660
678
  other_segment = segment.to_mutable().to_frozen()
661
679
  self.assertEqual(
662
680
  other_segment,
663
- segment_cls(
681
+ segment_cls.partial(
664
682
  c=point_cls(x=1.0, y=2.0),
665
683
  ),
666
684
  )
@@ -710,7 +728,9 @@ class ModuleInitializerTestCase(unittest.TestCase):
710
728
  module = self.init_test_module()
711
729
  json_value_cls = module["JsonValue"]
712
730
  json_object_cls = json_value_cls.Object
713
- 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
+ )
714
734
  self.assertEqual(json_object.kind, "object")
715
735
  self.assertEqual(json_object.value, json_object_cls.DEFAULT)
716
736
  self.assertEqual(
@@ -779,7 +799,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
779
799
  json_value_cls.wrap_object(
780
800
  json_value_cls.Object(
781
801
  entries=[
782
- json_value_cls.ObjectEntry(),
802
+ json_value_cls.ObjectEntry.partial(),
783
803
  json_value_cls.ObjectEntry.DEFAULT,
784
804
  ],
785
805
  )
@@ -814,7 +834,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
814
834
  json_value_cls = self.init_test_module()["JsonValue"]
815
835
  json_object_entry_cls = json_value_cls.ObjectEntry
816
836
  self.assertEqual(json_object_entry_cls.DEFAULT.value, json_value_cls.UNKNOWN)
817
- self.assertEqual(json_object_entry_cls().value, json_value_cls.UNKNOWN)
837
+ self.assertEqual(json_object_entry_cls.partial().value, json_value_cls.UNKNOWN)
818
838
 
819
839
  def test_enum_with_unrecognized_and_removed_fields(self):
820
840
  json_value_cls = self.init_test_module()["JsonValue"]
@@ -848,11 +868,18 @@ class ModuleInitializerTestCase(unittest.TestCase):
848
868
  module = self.init_test_module()
849
869
  point_cls = module["Point"]
850
870
  self.assertEqual(
851
- repr(point_cls(x=1.5)),
852
- "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
+ ),
853
880
  )
854
881
  self.assertEqual(
855
- repr(point_cls(x=1.5).to_mutable()),
882
+ repr(point_cls.partial(x=1.5).to_mutable()),
856
883
  "\n".join(
857
884
  [
858
885
  "Point.Mutable(",
@@ -874,8 +901,15 @@ class ModuleInitializerTestCase(unittest.TestCase):
874
901
  ),
875
902
  )
876
903
  self.assertEqual(
877
- repr(point_cls()),
878
- "Point()",
904
+ repr(point_cls.partial()),
905
+ "\n".join(
906
+ [
907
+ "Point(",
908
+ " x=0.0,",
909
+ " y=0.0,",
910
+ ")",
911
+ ]
912
+ ),
879
913
  )
880
914
  self.assertEqual(
881
915
  repr(point_cls.DEFAULT),
@@ -895,19 +929,22 @@ class ModuleInitializerTestCase(unittest.TestCase):
895
929
  shape_cls = module["Shape"]
896
930
  self.assertEqual(
897
931
  repr(shape_cls(points=[])),
898
- "Shape()",
932
+ "Shape(points=[])",
899
933
  )
900
934
  self.assertEqual(
901
935
  repr(shape_cls(points=[]).to_mutable()),
902
936
  "Shape.Mutable(points=[])",
903
937
  )
904
938
  self.assertEqual(
905
- repr(shape_cls(points=[point_cls(x=1.5)])),
939
+ repr(shape_cls(points=[point_cls(x=1.5, y=0.0)])),
906
940
  "\n".join(
907
941
  [
908
942
  "Shape(",
909
943
  " points=[",
910
- " Point(x=1.5),",
944
+ " Point(",
945
+ " x=1.5,",
946
+ " y=0.0,",
947
+ " ),",
911
948
  " ],",
912
949
  ")",
913
950
  ]
@@ -917,8 +954,8 @@ class ModuleInitializerTestCase(unittest.TestCase):
917
954
  repr(
918
955
  shape_cls(
919
956
  points=[
920
- point_cls(x=1.5),
921
- point_cls(y=2.5),
957
+ point_cls.partial(x=1.5),
958
+ point_cls.partial(y=2.5),
922
959
  ],
923
960
  )
924
961
  ),
@@ -926,8 +963,14 @@ class ModuleInitializerTestCase(unittest.TestCase):
926
963
  [
927
964
  "Shape(",
928
965
  " points=[",
929
- " Point(x=1.5),",
930
- " 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
+ " ),",
931
974
  " ],",
932
975
  ")",
933
976
  ]
@@ -937,8 +980,8 @@ class ModuleInitializerTestCase(unittest.TestCase):
937
980
  repr(
938
981
  shape_cls.Mutable(
939
982
  points=[
940
- point_cls(x=1.5),
941
- point_cls(y=2.5).to_mutable(),
983
+ point_cls.partial(x=1.5),
984
+ point_cls.partial(y=2.5).to_mutable(),
942
985
  ]
943
986
  )
944
987
  ),
@@ -946,7 +989,10 @@ class ModuleInitializerTestCase(unittest.TestCase):
946
989
  [
947
990
  "Shape.Mutable(",
948
991
  " points=[",
949
- " Point(x=1.5),",
992
+ " Point(",
993
+ " x=1.5,",
994
+ " y=0.0,",
995
+ " ),",
950
996
  " Point.Mutable(",
951
997
  " x=0.0,",
952
998
  " y=2.5,",
@@ -990,15 +1036,15 @@ class ModuleInitializerTestCase(unittest.TestCase):
990
1036
  ),
991
1037
  )
992
1038
  self.assertEqual(
993
- repr(json_value_cls.wrap_object(json_object_cls().DEFAULT)),
1039
+ repr(json_value_cls.wrap_object(json_object_cls.DEFAULT)),
994
1040
  "JsonValue.wrap_object(JsonValue.Object.DEFAULT)",
995
1041
  )
996
1042
  self.assertEqual(
997
- repr(json_value_cls.wrap_object(json_object_cls())),
1043
+ repr(json_value_cls.wrap_object(json_object_cls.partial())),
998
1044
  "\n".join(
999
1045
  [
1000
1046
  "JsonValue.wrap_object(",
1001
- " JsonValue.Object()",
1047
+ " JsonValue.Object(entries=[])",
1002
1048
  ")",
1003
1049
  ]
1004
1050
  ),
@@ -1081,6 +1127,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
1081
1127
  segment = segment_cls(
1082
1128
  a=point_cls(x=1.0, y=2.0),
1083
1129
  b=point_cls(x=3.0, y=4.0),
1130
+ c=None,
1084
1131
  ).to_mutable()
1085
1132
  a = segment.mutable_a
1086
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