errorz 0.2.0__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: errorz
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Represent fallible computations as values, instead of using exceptions.
5
5
  Author: Fábio Macêdo Mendes
6
6
  Author-email: Fábio Macêdo Mendes <fabiomacedomendes@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "errorz"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Represent fallible computations as values, instead of using exceptions."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -28,6 +28,7 @@ from __future__ import annotations
28
28
 
29
29
  import builtins
30
30
  import functools
31
+ from collections import deque
31
32
  from types import NotImplementedType
32
33
  from typing import (
33
34
  Any,
@@ -35,8 +36,10 @@ from typing import (
35
36
  Generic,
36
37
  Hashable,
37
38
  Iterable,
39
+ Iterator,
38
40
  Literal,
39
41
  Mapping,
42
+ NamedTuple,
40
43
  Optional,
41
44
  Protocol,
42
45
  Self,
@@ -85,6 +88,7 @@ class Err(Generic[E_co]):
85
88
  """
86
89
 
87
90
  __is_errorz_error__ = True
91
+ __match_args__ = ("error",)
88
92
 
89
93
  @property
90
94
  def error(self) -> E_co:
@@ -215,6 +219,15 @@ class Err(Generic[E_co]):
215
219
  return UnwrapError(e)
216
220
 
217
221
 
222
+ class Tagged[T, B: bool](NamedTuple):
223
+ value: T
224
+ is_error: B
225
+
226
+
227
+ type IsErr[E] = Tagged[E, Literal[True]]
228
+ type IsOk[T] = Tagged[T, Literal[False]]
229
+
230
+
218
231
  def _any(x: object) -> Any:
219
232
  return x
220
233
 
@@ -325,6 +338,27 @@ def check[T](value: Result[T], /) -> TypeIs[T]:
325
338
  return True
326
339
 
327
340
 
341
+ def tagged[T, E](value: Result[T, E], /) -> IsOk[T] | IsErr[E]:
342
+ """
343
+ Check if the value is an error and return False if it is, True otherwise.
344
+
345
+ Args:
346
+ value: The result value to check.
347
+
348
+ Returns:
349
+ True if the value is not an error, False otherwise.
350
+
351
+ Examples:
352
+ >>> rz.tagged(42)
353
+ Tagged(value=42, is_error=False)
354
+ >>> rz.tagged(rz.err('error'))
355
+ Tagged(value='error', is_error=True)
356
+ """
357
+ if isinstance(value, Err):
358
+ return cast("IsErr[E]", Tagged(value=value.error, is_error=True))
359
+ return cast("IsOk[T]", Tagged(value=value, is_error=False))
360
+
361
+
328
362
  #
329
363
  # Unwrappers
330
364
  #
@@ -413,6 +447,62 @@ def expect[T](value: Result[T], /, error: str | BaseException) -> T:
413
447
  raise UnwrapError(error)
414
448
 
415
449
 
450
+ def extract[T, E](fn: Callable[[E], T], value: Result[T, E], /) -> T:
451
+ """
452
+ Extract the value from the result.
453
+
454
+ If it is an error, transform it with the given function.
455
+
456
+ Args:
457
+ fn: The function to apply in case of errors.
458
+ value: The result value to extract the error from.
459
+
460
+ Examples:
461
+ >>> rz.extract(lambda e: f"error: {e}", rz.err(42))
462
+ 'error: 42'
463
+ >>> rz.extract(lambda e: str(e), "ok")
464
+ 'ok'
465
+ """
466
+ return fn(value.error) if isinstance(value, Err) else value
467
+
468
+
469
+ def unpack[T, E, R](
470
+ value: Result[T, E], /, ok: Callable[[T], R], err: Callable[[E], R]
471
+ ) -> R:
472
+ """
473
+ Unpack a result value using either the ok or err functions.
474
+
475
+ This is useful for chaining operations that may return errors without having to
476
+ check for errors at each step.
477
+
478
+ Args:
479
+ value:
480
+ The result value to unpack.
481
+ ok:
482
+ The function to apply if the value is an ok value.
483
+ err:
484
+ The function to apply if the value is an error.
485
+
486
+ Examples:
487
+ >>> rz.unpack(rz.err("error"), ok=str, err=str.upper)
488
+ 'ERROR'
489
+ >>> rz.unpack(42, ok=str, err=str.upper)
490
+ '42'
491
+
492
+ Notes:
493
+ This can be used as a poor-man's match statement. Real match statements
494
+ are supported, but there is no explicit Ok() case:
495
+
496
+ >>> result = rz.err("error")
497
+ >>> match result:
498
+ ... case rz.Err(error):
499
+ ... pass # code in the error case
500
+ ... case value:
501
+ ... pass # code in the ok case
502
+ """
503
+ return err(value.error) if isinstance(value, Err) else ok(value)
504
+
505
+
416
506
  def to_optional[T](value: Result[T, Any]) -> Optional[T]:
417
507
  """
418
508
  Convert a Result to an Optional by converting any error to None.
@@ -429,6 +519,40 @@ def to_optional[T](value: Result[T, Any]) -> Optional[T]:
429
519
  return value
430
520
 
431
521
 
522
+ def error[Any, E](value: Result[Any, E], /) -> Optional[E]:
523
+ """
524
+ Get the error value if the result is an error, None otherwise.
525
+
526
+ Args:
527
+ value: The result value to check.
528
+ Examples:
529
+ >>> rz.error(42) is None
530
+ True
531
+ >>> rz.error(rz.err('error'))
532
+ 'error'
533
+ """
534
+ if isinstance(value, Err):
535
+ return value.error
536
+ return None
537
+
538
+
539
+ def swap[T, E](value: Result[T, E], /) -> Result[E, T]:
540
+ """
541
+ Swap the ok and error values of a result.
542
+
543
+ Args:
544
+ value: The result value to swap.
545
+ Examples:
546
+ >>> rz.swap(42)
547
+ Err(42)
548
+ >>> rz.swap(rz.err('error'))
549
+ 'error'
550
+ """
551
+ if isinstance(value, Err):
552
+ return value.error
553
+ return Err(value)
554
+
555
+
432
556
  @overload
433
557
  def map[T, E, R](fn: Callable[[T], R], value: Result[T, E], /) -> Result[R, E]: ...
434
558
 
@@ -543,6 +667,28 @@ def map(fn: Any, *values: Any) -> Any:
543
667
  return fn(*values)
544
668
 
545
669
 
670
+ def map_err[T, E, R](fn: Callable[[E], R], value: Result[T, E], /) -> Result[T, R]:
671
+ """
672
+ Apply the function to the error value if it is an error, otherwise return
673
+ the ok value.
674
+
675
+ Args:
676
+ fn:
677
+ The function to apply to the error case.
678
+ value:
679
+ The result value to check.
680
+
681
+ Examples:
682
+ >>> rz.map_err(lambda e: f"error: {e}", rz.err(42))
683
+ Err('error: 42')
684
+ >>> rz.map_err(lambda e: f"error: {e}", 42)
685
+ 42
686
+ """
687
+ if isinstance(value, Err):
688
+ return Err(fn(value.error))
689
+ return value
690
+
691
+
546
692
  @overload
547
693
  def coalesce[T, E](values: Iterable[Result[T, E]], /) -> Result[T, E]: ...
548
694
 
@@ -617,32 +763,143 @@ def coalesce[T1, T2, T3, T4, T5, T6, T7, T8, E](
617
763
 
618
764
  def coalesce(*values: Any) -> Any:
619
765
  """
620
- Return the first value that is not an error, or the first error if all values are errors.
766
+ Return the first value that is not an error, or the last error if all values
767
+ are errors.
768
+
769
+ This function works as short-circuiting "or" as if Ok values are thruthy
770
+ and Err values are falsy.
621
771
 
622
772
  Examples:
623
773
  >>> rz.coalesce(rz.err("e1"), rz.err("e2"), 42, 43, ...)
624
774
  42
625
775
  >>> rz.coalesce(rz.err("e1"), rz.err("e2"))
626
- Err('e1')
776
+ Err('e2')
627
777
  >>> rz.coalesce([rz.err("e1"), rz.err("e2"), 42, 43, ...])
628
778
  42
629
779
 
630
780
  See also:
631
781
  - :func:`rz.zip`
632
- - :func:`rz.values`
782
+ - :func:`rz.all`
633
783
  """
634
784
  if len(values) == 1:
635
785
  values = values[0]
636
786
 
637
- err: Err[Any] | None = None
787
+ value: Any = None
638
788
  for value in values:
639
789
  if not isinstance(value, Err):
640
790
  return value
641
- elif err is None:
642
- err = value
643
- if err is None:
791
+ if value is None:
644
792
  raise ValueError("coalesce() expected at least one value")
645
- return err
793
+ return value
794
+
795
+
796
+ @overload
797
+ def all[T, E](values: Iterable[Result[T, E]], /) -> Result[T, E]: ...
798
+
799
+
800
+ @overload
801
+ def all[T1, T2, E1, E2](
802
+ x1: Result[T1, E1], x2: Result[T2, E2], /
803
+ ) -> Result[T1 | T2, E1 | E2]: ...
804
+
805
+
806
+ @overload
807
+ def all[T1, T2, T3, E](
808
+ x1: Result[T1, E], x2: Result[T2, E], x3: Result[T3, E], /
809
+ ) -> Result[T1 | T2 | T3, E]: ...
810
+
811
+
812
+ @overload
813
+ def all[T1, T2, T3, T4, E](
814
+ x1: Result[T1, E], x2: Result[T2, E], x3: Result[T3, E], x4: Result[T4, E], /
815
+ ) -> Result[T1 | T2 | T3 | T4, E]: ...
816
+
817
+
818
+ @overload
819
+ def all[T1, T2, T3, T4, T5, E](
820
+ x1: Result[T1, E],
821
+ x2: Result[T2, E],
822
+ x3: Result[T3, E],
823
+ x4: Result[T4, E],
824
+ x5: Result[T5, E],
825
+ /,
826
+ ) -> Result[T1 | T2 | T3 | T4 | T5, E]: ...
827
+
828
+
829
+ @overload
830
+ def all[T1, T2, T3, T4, T5, T6, E](
831
+ x1: Result[T1, E],
832
+ x2: Result[T2, E],
833
+ x3: Result[T3, E],
834
+ x4: Result[T4, E],
835
+ x5: Result[T5, E],
836
+ x6: Result[T6, E],
837
+ /,
838
+ ) -> Result[T1 | T2 | T3 | T4 | T5 | T6, E]: ...
839
+
840
+
841
+ @overload
842
+ def all[T1, T2, T3, T4, T5, T6, T7, E](
843
+ x1: Result[T1, E],
844
+ x2: Result[T2, E],
845
+ x3: Result[T3, E],
846
+ x4: Result[T4, E],
847
+ x5: Result[T5, E],
848
+ x6: Result[T6, E],
849
+ x7: Result[T7, E],
850
+ /,
851
+ ) -> Result[T1 | T2 | T3 | T4 | T5 | T6 | T7, E]: ...
852
+
853
+
854
+ @overload
855
+ def all[T1, T2, T3, T4, T5, T6, T7, T8, E](
856
+ x1: Result[T1, E],
857
+ x2: Result[T2, E],
858
+ x3: Result[T3, E],
859
+ x4: Result[T4, E],
860
+ x5: Result[T5, E],
861
+ x6: Result[T6, E],
862
+ x7: Result[T7, E],
863
+ x8: Result[T8, E],
864
+ /,
865
+ ) -> Result[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8, E]: ...
866
+
867
+
868
+ def all(*values: Any) -> Any:
869
+ """
870
+ Return the first error in the arguments. If no argument is an error, return
871
+ the last ok value.
872
+
873
+ This function works as short-circuiting "and" as if Ok values are thruthy
874
+ and Err values are falsy.
875
+
876
+ Args:
877
+ x1, x2, ...:
878
+ The result values to check.
879
+ It accepts any number of arguments (but only type checks up to 8).
880
+
881
+ Examples:
882
+ >>> rz.all(1, 2, 3)
883
+ 3
884
+ >>> rz.all(1, rz.err("e1"), rz.err("e2"))
885
+ Err('e1')
886
+
887
+ See also:
888
+ - :func:`rz.coalesce`
889
+ """
890
+ if len(values) == 1:
891
+ values = values[0]
892
+
893
+ if len(values) == 1:
894
+ values = values[0]
895
+
896
+ for value in values:
897
+ if isinstance(value, Err):
898
+ return value
899
+ try:
900
+ return value # pyright: ignore[reportPossiblyUnboundVariable]
901
+ except NameError:
902
+ raise ValueError("all() expected at least one value")
646
903
 
647
904
 
648
905
  @overload
@@ -962,7 +1219,7 @@ def some[T, E](value: Result[T, E]) -> Iterable[T]:
962
1219
 
963
1220
  Examples:
964
1221
  >>> list(rz.some(42))
965
- [42]s
1222
+ [42]
966
1223
  >>> list(rz.some(rz.err('error')))
967
1224
  []
968
1225
  """
@@ -995,14 +1252,86 @@ def filter[T, E](seq: Iterable[Result[T, E]], /) -> Iterable[T]:
995
1252
  seq: The sequence of fallible values to filter.
996
1253
 
997
1254
  Examples:
998
- >>> list(rz.values([1, 2, rz.err('error'), 3]))
1255
+ >>> list(rz.filter([1, 2, rz.err('error'), 3]))
999
1256
  [1, 2, 3]
1000
- >>> list(rz.values([rz.err("error 1"), rz.err("error 2")]))
1257
+ >>> list(rz.filter([rz.err("error 1"), rz.err("error 2")]))
1001
1258
  []
1002
1259
  """
1003
1260
  return (x for x in seq if not isinstance(x, Err))
1004
1261
 
1005
1262
 
1263
+ def partition[T, E](seq: Iterable[Result[T, E]], /) -> tuple[Iterable[T], Iterable[E]]:
1264
+ """
1265
+ Partition the given sequence into two sequence: one of non-error values and one of error values.
1266
+
1267
+ Args:
1268
+ seq: The sequence of fallible values to partition.
1269
+
1270
+ Examples:
1271
+ >>> oks, errs = rz.partition([1, 2, rz.err('error'), 3])
1272
+ >>> list(oks)
1273
+ [1, 2, 3]
1274
+ >>> list(errs)
1275
+ ['error']
1276
+ """
1277
+ next = _iter(seq).__next__
1278
+ seen_values = deque["T"]()
1279
+ seen_errors = deque["E"]()
1280
+
1281
+ def oks() -> Iterator[T]:
1282
+ while True:
1283
+ try:
1284
+ if seen_values:
1285
+ yield seen_values.popleft()
1286
+ continue
1287
+ if isinstance(value := next(), Err):
1288
+ seen_errors.append(value.error)
1289
+ continue
1290
+ yield value
1291
+ except StopIteration:
1292
+ return
1293
+
1294
+ def errors() -> Iterator[E]:
1295
+ while True:
1296
+ try:
1297
+ if seen_errors:
1298
+ yield seen_errors.popleft()
1299
+ continue
1300
+ if isinstance(value := next(), Err):
1301
+ seen_errors.append(value.error)
1302
+ continue
1303
+ seen_values.append(value)
1304
+ except StopIteration:
1305
+ return
1306
+
1307
+ # Is it faster to use itertools.tee?
1308
+ return oks(), errors()
1309
+
1310
+
1311
+ def combine[T, E](seq: Iterable[Result[T, E]], /) -> Result[list[T], E]:
1312
+ """
1313
+ Combine the given sequence of fallible values into a single list if all
1314
+ values are non-errors.
1315
+
1316
+ Return the first error found otherwise.
1317
+
1318
+ Args:
1319
+ seq: The sequence of fallible values to combine.
1320
+
1321
+ Examples:
1322
+ >>> rz.combine([1, 2, 3])
1323
+ [1, 2, 3]
1324
+ >>> rz.combine([1, 2, rz.err('error')])
1325
+ Err('error')
1326
+ """
1327
+ values = []
1328
+ for value in seq:
1329
+ if isinstance(value, Err):
1330
+ return value
1331
+ values.append(value)
1332
+ return values
1333
+
1334
+
1006
1335
  def filter_values[K, V, E](seq: Mapping[K, Result[V, E]], /) -> dict[K, V]:
1007
1336
  """
1008
1337
  Remove all Err values from the given mapping.
@@ -1039,7 +1368,7 @@ def non_empty[T](seq: Iterable[T], /, error: Any = MISSING) -> Result[list[T], A
1039
1368
  >>> rz.non_empty([1, 2, 3])
1040
1369
  [1, 2, 3]
1041
1370
  >>> rz.non_empty([])
1042
- Err(IndexError())
1371
+ Err(IndexError(...))
1043
1372
  >>> rz.non_empty([], error="Sequence is empty")
1044
1373
  Err('Sequence is empty')
1045
1374
  """
@@ -1071,9 +1400,9 @@ def single[T](seq: Iterable[T], /, error: Any = MISSING) -> Result[T, Any]:
1071
1400
  >>> rz.single([42])
1072
1401
  42
1073
1402
  >>> rz.single([])
1074
- Err(ValueError())
1403
+ Err(IndexError(...))
1075
1404
  >>> rz.single([1, 2])
1076
- Err(ValueError())
1405
+ Err(IndexError(...))
1077
1406
  >>> rz.single([], error="No single value")
1078
1407
  Err('No single value')
1079
1408
  """
@@ -0,0 +1,10 @@
1
+ import doctest
2
+
3
+ import errorz
4
+
5
+ if __name__ == "__main__":
6
+ doctest.testmod(
7
+ errorz,
8
+ globs={"rz": errorz},
9
+ optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE,
10
+ )
@@ -1,6 +0,0 @@
1
- import doctest
2
-
3
- import errorz
4
-
5
- if __name__ == "__main__":
6
- doctest.testmod(errorz, globs={"rz": errorz})
File without changes
File without changes
File without changes
File without changes