soia-client 1.0.30__tar.gz → 1.1.1__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.1}/PKG-INFO +1 -1
  2. {soia_client-1.0.30 → soia_client-1.1.1}/pyproject.toml +1 -1
  3. {soia_client-1.0.30 → soia_client-1.1.1}/soia/__init__.py +3 -0
  4. soia_client-1.1.1/soia/_impl/keep.py +20 -0
  5. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/primitives.py +5 -2
  6. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/structs.py +90 -21
  7. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/timestamp.py +41 -4
  8. {soia_client-1.0.30 → soia_client-1.1.1}/soia_client.egg-info/PKG-INFO +1 -1
  9. {soia_client-1.0.30 → soia_client-1.1.1}/soia_client.egg-info/SOURCES.txt +1 -0
  10. {soia_client-1.0.30 → soia_client-1.1.1}/tests/test_module_initializer.py +93 -40
  11. {soia_client-1.0.30 → soia_client-1.1.1}/tests/test_timestamp.py +20 -2
  12. {soia_client-1.0.30 → soia_client-1.1.1}/LICENSE +0 -0
  13. {soia_client-1.0.30 → soia_client-1.1.1}/README.md +0 -0
  14. {soia_client-1.0.30 → soia_client-1.1.1}/setup.cfg +0 -0
  15. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/__init__.py +0 -0
  16. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/arrays.py +0 -0
  17. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/enums.py +0 -0
  18. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/function_maker.py +0 -0
  19. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/keyed_items.py +0 -0
  20. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/method.py +0 -0
  21. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/never.py +0 -0
  22. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/optionals.py +0 -0
  23. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/repr.py +0 -0
  24. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/serializer.py +0 -0
  25. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/serializers.py +0 -0
  26. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/service.py +0 -0
  27. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/service_client.py +0 -0
  28. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_impl/type_adapter.py +0 -0
  29. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_module_initializer.py +0 -0
  30. {soia_client-1.0.30 → soia_client-1.1.1}/soia/_spec.py +0 -0
  31. {soia_client-1.0.30 → soia_client-1.1.1}/soia/reflection.py +0 -0
  32. {soia_client-1.0.30 → soia_client-1.1.1}/soia_client.egg-info/dependency_links.txt +0 -0
  33. {soia_client-1.0.30 → soia_client-1.1.1}/soia_client.egg-info/top_level.txt +0 -0
  34. {soia_client-1.0.30 → soia_client-1.1.1}/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.1
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.1"
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 = ["_self"]
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, f" 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="replace",
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.1
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,17 @@ 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)
371
+
372
+ def test_replace(self):
373
+ point_cls = self.init_test_module()["Point"]
374
+ point = point_cls(x=1.5, y=2.5).replace(y=3.5)
375
+ self.assertEqual(point.x, 1.5)
376
+ self.assertEqual(point.y, 3.5)
370
377
 
371
378
  def test_to_mutable(self):
372
379
  point_cls = self.init_test_module()["Point"]
@@ -387,7 +394,8 @@ class ModuleInitializerTestCase(unittest.TestCase):
387
394
  self.assertEqual(hash(a), hash(b))
388
395
  self.assertNotEqual(a, c)
389
396
  self.assertNotEqual(a, "foo")
390
- self.assertEqual(point_cls(), point_cls(x=0.0, y=0.0))
397
+ self.assertEqual(point_cls.partial(), point_cls(x=0.0, y=0.0))
398
+ self.assertEqual(point_cls.DEFAULT, point_cls(x=0.0, y=0.0))
391
399
 
392
400
  def test_or_mutable(self):
393
401
  point_cls = self.init_test_module()["Point"]
@@ -404,8 +412,9 @@ class ModuleInitializerTestCase(unittest.TestCase):
404
412
  i64=0,
405
413
  u64=0,
406
414
  t=Timestamp.EPOCH,
415
+ s="",
407
416
  )
408
- b = primitives_cls()
417
+ b = primitives_cls.partial()
409
418
  self.assertEqual(a, b)
410
419
  self.assertEqual(hash(a), hash(b))
411
420
 
@@ -421,6 +430,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
421
430
  i64=2,
422
431
  u64=3,
423
432
  t=Timestamp.from_unix_millis(4),
433
+ s="",
424
434
  )
425
435
  self.assertEqual(serializer.to_json(p), [1, "YQ==", 3.14, 3.14, 1, 2, 3, "", 4])
426
436
 
@@ -440,6 +450,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
440
450
  i64=2,
441
451
  u64=3,
442
452
  t=Timestamp.from_unix_millis(4),
453
+ s="",
443
454
  ),
444
455
  )
445
456
 
@@ -454,10 +465,11 @@ class ModuleInitializerTestCase(unittest.TestCase):
454
465
  i64=2,
455
466
  u64=3,
456
467
  t=Timestamp.from_unix_millis(4),
468
+ s="",
457
469
  )
458
470
  self.assertEqual(
459
471
  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)",
472
+ "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
473
  )
462
474
 
463
475
  def test_from_json_converts_between_ints_and_floats(self):
@@ -502,7 +514,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
502
514
  point_cls = self.init_test_module()["Point"]
503
515
  serializer = point_cls.SERIALIZER
504
516
  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))
517
+ self.assertEqual(serializer.from_json([1.5]), point_cls(x=1.5, y=0.0))
506
518
  self.assertEqual(serializer.from_json([0.0]), point_cls.DEFAULT)
507
519
  self.assertEqual(serializer.from_json(0), point_cls.DEFAULT)
508
520
 
@@ -533,28 +545,30 @@ class ModuleInitializerTestCase(unittest.TestCase):
533
545
  foobar_cls = test_module["Foobar"]
534
546
  point_cls = test_module["Point"]
535
547
  serializer = foobar_cls.SERIALIZER
536
- foobar = foobar_cls()
548
+ foobar = foobar_cls.partial()
537
549
  self.assertEqual(serializer.to_json_code(foobar), "[]")
538
550
  self.assertEqual(serializer.from_json_code("[]"), foobar)
539
- foobar = foobar_cls(a=3)
551
+ foobar = foobar_cls.partial(a=3)
540
552
  self.assertEqual(serializer.to_json_code(foobar), "[0,3]")
541
553
  self.assertEqual(serializer.from_json_code("[0,3]"), foobar)
542
554
  self.assertEqual(serializer.from_json_code("[0,3.1]"), foobar)
543
- foobar = foobar_cls(b=3, point=point_cls.DEFAULT)
555
+ foobar = foobar_cls(a=0, b=3, point=point_cls.DEFAULT)
544
556
  self.assertEqual(serializer.to_json_code(foobar), "[0,0,0,3]")
545
557
  self.assertEqual(serializer.from_json_code("[0,0,0,3]"), foobar)
546
558
  self.assertEqual(serializer.from_json_code("[0,0,0,3.1]"), foobar)
547
- foobar = foobar_cls(point=point_cls(x=2))
559
+ foobar = foobar_cls.partial(point=point_cls.partial(x=2))
548
560
  self.assertEqual(serializer.to_json_code(foobar), "[0,0,0,0,[2.0]]")
549
561
  self.assertEqual(serializer.from_json_code("[0,0,0,0,[2.0]]"), foobar)
550
562
 
551
563
  def test_recursive_struct(self):
552
564
  rec_cls = self.init_test_module()["Rec"]
553
- r = rec_cls(r=rec_cls(r=rec_cls.DEFAULT, x=1))
565
+ r = rec_cls.partial(r=rec_cls(r=rec_cls.DEFAULT, x=1))
554
566
  serializer = rec_cls.SERIALIZER
555
567
  self.assertEqual(serializer.to_json_code(r), "[[[],1]]")
556
568
  self.assertEqual(serializer.from_json_code("[[[],1]]"), r)
557
- self.assertEqual(str(r), "Rec(\n r=Rec(x=1),\n)")
569
+ self.assertEqual(
570
+ str(r), "Rec(\n r=Rec(\n r=Rec.DEFAULT,\n x=1,\n ),\n x=0,\n)"
571
+ )
558
572
 
559
573
  def test_struct_ctor_accepts_mutable_struct(self):
560
574
  module = self.init_test_module()
@@ -563,6 +577,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
563
577
  segment = segment_cls(
564
578
  a=point_cls(x=1.0, y=2.0).to_mutable(),
565
579
  b=point_cls(x=3.0, y=4.0),
580
+ c=None,
566
581
  )
567
582
  self.assertEqual(
568
583
  segment,
@@ -577,7 +592,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
577
592
  module = self.init_test_module()
578
593
  segment_cls = module["Segment"]
579
594
  try:
580
- segment_cls(
595
+ segment_cls.partial(
581
596
  # Should be a Point
582
597
  a=segment_cls.DEFAULT,
583
598
  )
@@ -587,9 +602,18 @@ class ModuleInitializerTestCase(unittest.TestCase):
587
602
 
588
603
  def test_struct_ctor_raises_error_if_unknown_arg(self):
589
604
  module = self.init_test_module()
590
- segment_cls = module["Segment"]
605
+ point_cls = module["Point"]
591
606
  try:
592
- segment_cls(foo=4)
607
+ point_cls(x=1, b=2, foo=4)
608
+ self.fail("Expected to fail")
609
+ except Exception:
610
+ pass
611
+
612
+ def test_struct_ctor_raises_error_if_missing_arg(self):
613
+ module = self.init_test_module()
614
+ point_cls = module["Point"]
615
+ try:
616
+ point_cls(x=1)
593
617
  self.fail("Expected to fail")
594
618
  except Exception:
595
619
  pass
@@ -654,13 +678,13 @@ class ModuleInitializerTestCase(unittest.TestCase):
654
678
  module = self.init_test_module()
655
679
  segment_cls = module["Segment"]
656
680
  point_cls = module["Point"]
657
- segment = segment_cls(
681
+ segment = segment_cls.partial(
658
682
  c=point_cls.Mutable(x=1.0, y=2.0),
659
683
  )
660
684
  other_segment = segment.to_mutable().to_frozen()
661
685
  self.assertEqual(
662
686
  other_segment,
663
- segment_cls(
687
+ segment_cls.partial(
664
688
  c=point_cls(x=1.0, y=2.0),
665
689
  ),
666
690
  )
@@ -710,7 +734,9 @@ class ModuleInitializerTestCase(unittest.TestCase):
710
734
  module = self.init_test_module()
711
735
  json_value_cls = module["JsonValue"]
712
736
  json_object_cls = json_value_cls.Object
713
- json_object = json_value_cls.wrap_object(json_object_cls().to_mutable())
737
+ json_object = json_value_cls.wrap_object(
738
+ json_object_cls(entries=[]).to_mutable()
739
+ )
714
740
  self.assertEqual(json_object.kind, "object")
715
741
  self.assertEqual(json_object.value, json_object_cls.DEFAULT)
716
742
  self.assertEqual(
@@ -779,7 +805,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
779
805
  json_value_cls.wrap_object(
780
806
  json_value_cls.Object(
781
807
  entries=[
782
- json_value_cls.ObjectEntry(),
808
+ json_value_cls.ObjectEntry.partial(),
783
809
  json_value_cls.ObjectEntry.DEFAULT,
784
810
  ],
785
811
  )
@@ -814,7 +840,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
814
840
  json_value_cls = self.init_test_module()["JsonValue"]
815
841
  json_object_entry_cls = json_value_cls.ObjectEntry
816
842
  self.assertEqual(json_object_entry_cls.DEFAULT.value, json_value_cls.UNKNOWN)
817
- self.assertEqual(json_object_entry_cls().value, json_value_cls.UNKNOWN)
843
+ self.assertEqual(json_object_entry_cls.partial().value, json_value_cls.UNKNOWN)
818
844
 
819
845
  def test_enum_with_unrecognized_and_removed_fields(self):
820
846
  json_value_cls = self.init_test_module()["JsonValue"]
@@ -848,11 +874,18 @@ class ModuleInitializerTestCase(unittest.TestCase):
848
874
  module = self.init_test_module()
849
875
  point_cls = module["Point"]
850
876
  self.assertEqual(
851
- repr(point_cls(x=1.5)),
852
- "Point(x=1.5)",
877
+ repr(point_cls.partial(x=1.5)),
878
+ "\n".join(
879
+ [
880
+ "Point(",
881
+ " x=1.5,",
882
+ " y=0.0,",
883
+ ")",
884
+ ]
885
+ ),
853
886
  )
854
887
  self.assertEqual(
855
- repr(point_cls(x=1.5).to_mutable()),
888
+ repr(point_cls.partial(x=1.5).to_mutable()),
856
889
  "\n".join(
857
890
  [
858
891
  "Point.Mutable(",
@@ -874,8 +907,15 @@ class ModuleInitializerTestCase(unittest.TestCase):
874
907
  ),
875
908
  )
876
909
  self.assertEqual(
877
- repr(point_cls()),
878
- "Point()",
910
+ repr(point_cls.partial()),
911
+ "\n".join(
912
+ [
913
+ "Point(",
914
+ " x=0.0,",
915
+ " y=0.0,",
916
+ ")",
917
+ ]
918
+ ),
879
919
  )
880
920
  self.assertEqual(
881
921
  repr(point_cls.DEFAULT),
@@ -895,19 +935,22 @@ class ModuleInitializerTestCase(unittest.TestCase):
895
935
  shape_cls = module["Shape"]
896
936
  self.assertEqual(
897
937
  repr(shape_cls(points=[])),
898
- "Shape()",
938
+ "Shape(points=[])",
899
939
  )
900
940
  self.assertEqual(
901
941
  repr(shape_cls(points=[]).to_mutable()),
902
942
  "Shape.Mutable(points=[])",
903
943
  )
904
944
  self.assertEqual(
905
- repr(shape_cls(points=[point_cls(x=1.5)])),
945
+ repr(shape_cls(points=[point_cls(x=1.5, y=0.0)])),
906
946
  "\n".join(
907
947
  [
908
948
  "Shape(",
909
949
  " points=[",
910
- " Point(x=1.5),",
950
+ " Point(",
951
+ " x=1.5,",
952
+ " y=0.0,",
953
+ " ),",
911
954
  " ],",
912
955
  ")",
913
956
  ]
@@ -917,8 +960,8 @@ class ModuleInitializerTestCase(unittest.TestCase):
917
960
  repr(
918
961
  shape_cls(
919
962
  points=[
920
- point_cls(x=1.5),
921
- point_cls(y=2.5),
963
+ point_cls.partial(x=1.5),
964
+ point_cls.partial(y=2.5),
922
965
  ],
923
966
  )
924
967
  ),
@@ -926,8 +969,14 @@ class ModuleInitializerTestCase(unittest.TestCase):
926
969
  [
927
970
  "Shape(",
928
971
  " points=[",
929
- " Point(x=1.5),",
930
- " Point(y=2.5),",
972
+ " Point(",
973
+ " x=1.5,",
974
+ " y=0.0,",
975
+ " ),",
976
+ " Point(",
977
+ " x=0.0,",
978
+ " y=2.5,",
979
+ " ),",
931
980
  " ],",
932
981
  ")",
933
982
  ]
@@ -937,8 +986,8 @@ class ModuleInitializerTestCase(unittest.TestCase):
937
986
  repr(
938
987
  shape_cls.Mutable(
939
988
  points=[
940
- point_cls(x=1.5),
941
- point_cls(y=2.5).to_mutable(),
989
+ point_cls.partial(x=1.5),
990
+ point_cls.partial(y=2.5).to_mutable(),
942
991
  ]
943
992
  )
944
993
  ),
@@ -946,7 +995,10 @@ class ModuleInitializerTestCase(unittest.TestCase):
946
995
  [
947
996
  "Shape.Mutable(",
948
997
  " points=[",
949
- " Point(x=1.5),",
998
+ " Point(",
999
+ " x=1.5,",
1000
+ " y=0.0,",
1001
+ " ),",
950
1002
  " Point.Mutable(",
951
1003
  " x=0.0,",
952
1004
  " y=2.5,",
@@ -990,15 +1042,15 @@ class ModuleInitializerTestCase(unittest.TestCase):
990
1042
  ),
991
1043
  )
992
1044
  self.assertEqual(
993
- repr(json_value_cls.wrap_object(json_object_cls().DEFAULT)),
1045
+ repr(json_value_cls.wrap_object(json_object_cls.DEFAULT)),
994
1046
  "JsonValue.wrap_object(JsonValue.Object.DEFAULT)",
995
1047
  )
996
1048
  self.assertEqual(
997
- repr(json_value_cls.wrap_object(json_object_cls())),
1049
+ repr(json_value_cls.wrap_object(json_object_cls.partial())),
998
1050
  "\n".join(
999
1051
  [
1000
1052
  "JsonValue.wrap_object(",
1001
- " JsonValue.Object()",
1053
+ " JsonValue.Object(entries=[])",
1002
1054
  ")",
1003
1055
  ]
1004
1056
  ),
@@ -1081,6 +1133,7 @@ class ModuleInitializerTestCase(unittest.TestCase):
1081
1133
  segment = segment_cls(
1082
1134
  a=point_cls(x=1.0, y=2.0),
1083
1135
  b=point_cls(x=3.0, y=4.0),
1136
+ c=None,
1084
1137
  ).to_mutable()
1085
1138
  a = segment.mutable_a
1086
1139
  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