errorz 0.1.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.
- {errorz-0.1.0 → errorz-0.2.1}/PKG-INFO +1 -1
- {errorz-0.1.0 → errorz-0.2.1}/pyproject.toml +2 -1
- {errorz-0.1.0 → errorz-0.2.1}/src/errorz/__init__.py +490 -71
- errorz-0.2.1/src/errorz/__main__.py +10 -0
- {errorz-0.1.0 → errorz-0.2.1}/src/errorz/hypothesis.py +22 -4
- errorz-0.1.0/src/errorz/__main__.py +0 -6
- {errorz-0.1.0 → errorz-0.2.1}/README.md +0 -0
- {errorz-0.1.0 → errorz-0.2.1}/src/errorz/mypy.py +0 -0
- {errorz-0.1.0 → errorz-0.2.1}/src/errorz/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "errorz"
|
|
3
|
-
version = "0.1
|
|
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 = [
|
|
@@ -53,6 +53,7 @@ dev = [
|
|
|
53
53
|
"sphinx-mdinclude>=0.6.2",
|
|
54
54
|
"taskipy>=1.14.1",
|
|
55
55
|
]
|
|
56
|
+
hypothesis = ["hypothesis>=6.155.2"]
|
|
56
57
|
|
|
57
58
|
[tool.taskipy.tasks]
|
|
58
59
|
docs = { cmd = "sphinx-build -b html docs/source docs/build", help = "Build HTML documentation" }
|
|
@@ -24,14 +24,22 @@ help vendorizing it. It is distributed as a package, instead of a errorz.py file
|
|
|
24
24
|
to allow for py.typed marker.
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
27
29
|
import builtins
|
|
28
30
|
import functools
|
|
31
|
+
from collections import deque
|
|
29
32
|
from types import NotImplementedType
|
|
30
33
|
from typing import (
|
|
31
34
|
Any,
|
|
32
35
|
Callable,
|
|
33
36
|
Generic,
|
|
37
|
+
Hashable,
|
|
34
38
|
Iterable,
|
|
39
|
+
Iterator,
|
|
40
|
+
Literal,
|
|
41
|
+
Mapping,
|
|
42
|
+
NamedTuple,
|
|
35
43
|
Optional,
|
|
36
44
|
Protocol,
|
|
37
45
|
Self,
|
|
@@ -56,9 +64,9 @@ __all__ = (
|
|
|
56
64
|
"expect",
|
|
57
65
|
"map",
|
|
58
66
|
"coalesce",
|
|
59
|
-
"
|
|
67
|
+
"filter",
|
|
60
68
|
"zip",
|
|
61
|
-
"
|
|
69
|
+
"some",
|
|
62
70
|
"getattr",
|
|
63
71
|
"call",
|
|
64
72
|
"iter",
|
|
@@ -73,7 +81,6 @@ T_co = TypeVar("T_co", covariant=True)
|
|
|
73
81
|
type Result[T, E = Any] = T | Err[E]
|
|
74
82
|
|
|
75
83
|
|
|
76
|
-
@final
|
|
77
84
|
class Err(Generic[E_co]):
|
|
78
85
|
"""
|
|
79
86
|
A simple error wrapper that implements the Error protocol. It is used to wrap
|
|
@@ -81,6 +88,7 @@ class Err(Generic[E_co]):
|
|
|
81
88
|
"""
|
|
82
89
|
|
|
83
90
|
__is_errorz_error__ = True
|
|
91
|
+
__match_args__ = ("error",)
|
|
84
92
|
|
|
85
93
|
@property
|
|
86
94
|
def error(self) -> E_co:
|
|
@@ -92,10 +100,14 @@ class Err(Generic[E_co]):
|
|
|
92
100
|
def __repr__(self) -> str:
|
|
93
101
|
return f"Err({self._error!r})"
|
|
94
102
|
|
|
103
|
+
def __hash__[E: Hashable](self: Err[E]) -> int:
|
|
104
|
+
return hash(self._error)
|
|
105
|
+
|
|
95
106
|
#
|
|
96
107
|
# Python magic methods
|
|
97
108
|
#
|
|
98
|
-
|
|
109
|
+
@final
|
|
110
|
+
def __bool__(self) -> Literal[False]:
|
|
99
111
|
return False
|
|
100
112
|
|
|
101
113
|
# Arithmetic operations
|
|
@@ -207,10 +219,20 @@ class Err(Generic[E_co]):
|
|
|
207
219
|
return UnwrapError(e)
|
|
208
220
|
|
|
209
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
|
+
|
|
210
231
|
def _any(x: object) -> Any:
|
|
211
232
|
return x
|
|
212
233
|
|
|
213
234
|
|
|
235
|
+
_iter = builtins.iter
|
|
214
236
|
_getattr = builtins.getattr
|
|
215
237
|
|
|
216
238
|
|
|
@@ -283,6 +305,10 @@ def is_err(value: Result[Any], /) -> TypeIs[Err]:
|
|
|
283
305
|
False
|
|
284
306
|
|
|
285
307
|
Notes:
|
|
308
|
+
The corresponding is_ok() does not exist because typecheckers often struggle
|
|
309
|
+
with type narrowing because Python type system do not include negative
|
|
310
|
+
bounds (e.g. "T but not Err").
|
|
311
|
+
|
|
286
312
|
The Err case evaluates to False. If you known that T is never falsy,
|
|
287
313
|
you can just use the common Pythonic ways of checking for nullable
|
|
288
314
|
values:
|
|
@@ -298,34 +324,39 @@ def is_err(value: Result[Any], /) -> TypeIs[Err]:
|
|
|
298
324
|
return isinstance(value, Err)
|
|
299
325
|
|
|
300
326
|
|
|
301
|
-
def
|
|
327
|
+
def check[T](value: Result[T], /) -> TypeIs[T]:
|
|
302
328
|
"""
|
|
303
|
-
Check if
|
|
329
|
+
Check if value is not an Err and return True.
|
|
304
330
|
|
|
305
|
-
|
|
306
|
-
value: The result value to check.
|
|
331
|
+
If the value is an error, raise it or a UnwrapError if E is not an exception.
|
|
307
332
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
False
|
|
311
|
-
>>> rz.is_ok(42)
|
|
312
|
-
True
|
|
333
|
+
Args:
|
|
334
|
+
value: The result value to validate.
|
|
313
335
|
"""
|
|
314
|
-
|
|
336
|
+
if isinstance(value, Err):
|
|
337
|
+
raise value.exception()
|
|
338
|
+
return True
|
|
315
339
|
|
|
316
340
|
|
|
317
|
-
def
|
|
341
|
+
def tagged[T, E](value: Result[T, E], /) -> IsOk[T] | IsErr[E]:
|
|
318
342
|
"""
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
If the value is an error, raise it or a UnwrapError if E is not an exception.
|
|
343
|
+
Check if the value is an error and return False if it is, True otherwise.
|
|
322
344
|
|
|
323
345
|
Args:
|
|
324
|
-
value: The result value to
|
|
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)
|
|
325
356
|
"""
|
|
326
357
|
if isinstance(value, Err):
|
|
327
|
-
|
|
328
|
-
return
|
|
358
|
+
return cast("IsErr[E]", Tagged(value=value.error, is_error=True))
|
|
359
|
+
return cast("IsOk[T]", Tagged(value=value, is_error=False))
|
|
329
360
|
|
|
330
361
|
|
|
331
362
|
#
|
|
@@ -416,6 +447,62 @@ def expect[T](value: Result[T], /, error: str | BaseException) -> T:
|
|
|
416
447
|
raise UnwrapError(error)
|
|
417
448
|
|
|
418
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
|
+
|
|
419
506
|
def to_optional[T](value: Result[T, Any]) -> Optional[T]:
|
|
420
507
|
"""
|
|
421
508
|
Convert a Result to an Optional by converting any error to None.
|
|
@@ -432,6 +519,40 @@ def to_optional[T](value: Result[T, Any]) -> Optional[T]:
|
|
|
432
519
|
return value
|
|
433
520
|
|
|
434
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
|
+
|
|
435
556
|
@overload
|
|
436
557
|
def map[T, E, R](fn: Callable[[T], R], value: Result[T, E], /) -> Result[R, E]: ...
|
|
437
558
|
|
|
@@ -546,6 +667,28 @@ def map(fn: Any, *values: Any) -> Any:
|
|
|
546
667
|
return fn(*values)
|
|
547
668
|
|
|
548
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
|
+
|
|
549
692
|
@overload
|
|
550
693
|
def coalesce[T, E](values: Iterable[Result[T, E]], /) -> Result[T, E]: ...
|
|
551
694
|
|
|
@@ -620,48 +763,143 @@ def coalesce[T1, T2, T3, T4, T5, T6, T7, T8, E](
|
|
|
620
763
|
|
|
621
764
|
def coalesce(*values: Any) -> Any:
|
|
622
765
|
"""
|
|
623
|
-
Return the first value that is not an error, or the
|
|
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.
|
|
624
771
|
|
|
625
772
|
Examples:
|
|
626
773
|
>>> rz.coalesce(rz.err("e1"), rz.err("e2"), 42, 43, ...)
|
|
627
774
|
42
|
|
628
775
|
>>> rz.coalesce(rz.err("e1"), rz.err("e2"))
|
|
629
|
-
Err('
|
|
776
|
+
Err('e2')
|
|
630
777
|
>>> rz.coalesce([rz.err("e1"), rz.err("e2"), 42, 43, ...])
|
|
631
778
|
42
|
|
632
779
|
|
|
633
780
|
See also:
|
|
634
781
|
- :func:`rz.zip`
|
|
635
|
-
- :func:`rz.
|
|
782
|
+
- :func:`rz.all`
|
|
636
783
|
"""
|
|
637
784
|
if len(values) == 1:
|
|
638
785
|
values = values[0]
|
|
639
786
|
|
|
640
|
-
|
|
787
|
+
value: Any = None
|
|
641
788
|
for value in values:
|
|
642
789
|
if not isinstance(value, Err):
|
|
643
790
|
return value
|
|
644
|
-
|
|
645
|
-
err = value
|
|
646
|
-
if err is None:
|
|
791
|
+
if value is None:
|
|
647
792
|
raise ValueError("coalesce() expected at least one value")
|
|
648
|
-
return
|
|
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]: ...
|
|
649
839
|
|
|
650
840
|
|
|
651
|
-
|
|
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:
|
|
652
869
|
"""
|
|
653
|
-
Return
|
|
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.
|
|
654
875
|
|
|
655
876
|
Args:
|
|
656
|
-
|
|
877
|
+
x1, x2, ...:
|
|
878
|
+
The result values to check.
|
|
879
|
+
It accepts any number of arguments (but only type checks up to 8).
|
|
657
880
|
|
|
658
881
|
Examples:
|
|
659
|
-
>>>
|
|
660
|
-
|
|
661
|
-
>>>
|
|
662
|
-
|
|
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`
|
|
663
889
|
"""
|
|
664
|
-
|
|
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")
|
|
665
903
|
|
|
666
904
|
|
|
667
905
|
@overload
|
|
@@ -761,40 +999,6 @@ def zip(*args: Any) -> Any:
|
|
|
761
999
|
return args
|
|
762
1000
|
|
|
763
1001
|
|
|
764
|
-
def elements[T, E](value: Result[T, E]) -> Iterable[T]:
|
|
765
|
-
"""
|
|
766
|
-
Yield nothing or the optional value, if it exists.
|
|
767
|
-
|
|
768
|
-
Args:
|
|
769
|
-
value: The optional value to yield.
|
|
770
|
-
|
|
771
|
-
Examples:
|
|
772
|
-
>>> list(rz.elements(42))
|
|
773
|
-
[42]
|
|
774
|
-
>>> list(rz.elements(rz.err('error')))
|
|
775
|
-
[]
|
|
776
|
-
"""
|
|
777
|
-
if not isinstance(value, Err):
|
|
778
|
-
yield value
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
def iter[T, E](value: Result[Iterable[T], E]) -> Iterable[T]:
|
|
782
|
-
"""
|
|
783
|
-
Yield nothing or the elements of an iterable.
|
|
784
|
-
|
|
785
|
-
Args:
|
|
786
|
-
value: The optional iterable to yield from.
|
|
787
|
-
|
|
788
|
-
Examples:
|
|
789
|
-
>>> list(rz.iter([42]))
|
|
790
|
-
[42]
|
|
791
|
-
>>> list(rz.iter(rz.err('error')))
|
|
792
|
-
[]
|
|
793
|
-
"""
|
|
794
|
-
if not isinstance(value, Err):
|
|
795
|
-
yield from value
|
|
796
|
-
|
|
797
|
-
|
|
798
1002
|
class Caller[**P, R](Protocol):
|
|
799
1003
|
def __call__(
|
|
800
1004
|
self, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
|
|
@@ -1001,3 +1205,218 @@ def _wrap_safe[F: Callable[..., Any]](fn: Any, impl: F) -> F:
|
|
|
1001
1205
|
if ret_type is not None:
|
|
1002
1206
|
impl.__annotations__["return"] = Result[ret_type, Exception] # type: ignore
|
|
1003
1207
|
return impl
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
#
|
|
1211
|
+
# Lists, iterators, and other collections
|
|
1212
|
+
#
|
|
1213
|
+
def some[T, E](value: Result[T, E]) -> Iterable[T]:
|
|
1214
|
+
"""
|
|
1215
|
+
Yield nothing or the ok value, if it exists.
|
|
1216
|
+
|
|
1217
|
+
Args:
|
|
1218
|
+
value: The fallible value to yield.
|
|
1219
|
+
|
|
1220
|
+
Examples:
|
|
1221
|
+
>>> list(rz.some(42))
|
|
1222
|
+
[42]
|
|
1223
|
+
>>> list(rz.some(rz.err('error')))
|
|
1224
|
+
[]
|
|
1225
|
+
"""
|
|
1226
|
+
if not isinstance(value, Err):
|
|
1227
|
+
yield value
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def iter[T, E](value: Result[Iterable[T], E]) -> Iterable[T]:
|
|
1231
|
+
"""
|
|
1232
|
+
Yield nothing or the elements of an iterable.
|
|
1233
|
+
|
|
1234
|
+
Args:
|
|
1235
|
+
value: The fallible iterable to yield from.
|
|
1236
|
+
|
|
1237
|
+
Examples:
|
|
1238
|
+
>>> list(rz.iter([42]))
|
|
1239
|
+
[42]
|
|
1240
|
+
>>> list(rz.iter(rz.err('error')))
|
|
1241
|
+
[]
|
|
1242
|
+
"""
|
|
1243
|
+
if not isinstance(value, Err):
|
|
1244
|
+
yield from value
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
def filter[T, E](seq: Iterable[Result[T, E]], /) -> Iterable[T]:
|
|
1248
|
+
"""
|
|
1249
|
+
Return an iterable of the non-error values in the given sequence.
|
|
1250
|
+
|
|
1251
|
+
Args:
|
|
1252
|
+
seq: The sequence of fallible values to filter.
|
|
1253
|
+
|
|
1254
|
+
Examples:
|
|
1255
|
+
>>> list(rz.filter([1, 2, rz.err('error'), 3]))
|
|
1256
|
+
[1, 2, 3]
|
|
1257
|
+
>>> list(rz.filter([rz.err("error 1"), rz.err("error 2")]))
|
|
1258
|
+
[]
|
|
1259
|
+
"""
|
|
1260
|
+
return (x for x in seq if not isinstance(x, Err))
|
|
1261
|
+
|
|
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
|
+
|
|
1335
|
+
def filter_values[K, V, E](seq: Mapping[K, Result[V, E]], /) -> dict[K, V]:
|
|
1336
|
+
"""
|
|
1337
|
+
Remove all Err values from the given mapping.
|
|
1338
|
+
|
|
1339
|
+
Args:
|
|
1340
|
+
seq: The mapping of fallible values to filter.
|
|
1341
|
+
|
|
1342
|
+
Examples:
|
|
1343
|
+
>>> rz.filter_values({'a': 1, 'b': rz.err('error'), 'c': 3})
|
|
1344
|
+
{'a': 1, 'c': 3}
|
|
1345
|
+
>>> rz.filter_values({'a': rz.err("error 1"), 'b': rz.err("error 2")})
|
|
1346
|
+
{}
|
|
1347
|
+
"""
|
|
1348
|
+
return {k: v for k, v in seq.items() if not isinstance(v, Err)}
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
@overload
|
|
1352
|
+
def non_empty[T](seq: Iterable[T], /) -> Result[list[T], IndexError]: ...
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
@overload
|
|
1356
|
+
def non_empty[T, E](seq: Iterable[T], /, error: E) -> Result[list[T], E]: ...
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
def non_empty[T](seq: Iterable[T], /, error: Any = MISSING) -> Result[list[T], Any]:
|
|
1360
|
+
"""
|
|
1361
|
+
Return the given sequence if it contains at least one non-error value, or an Error otherwise.
|
|
1362
|
+
|
|
1363
|
+
Args:
|
|
1364
|
+
seq: The sequence of values to check.
|
|
1365
|
+
error: The error to return if the sequence is empty (defaults to IndexError).
|
|
1366
|
+
|
|
1367
|
+
Examples:
|
|
1368
|
+
>>> rz.non_empty([1, 2, 3])
|
|
1369
|
+
[1, 2, 3]
|
|
1370
|
+
>>> rz.non_empty([])
|
|
1371
|
+
Err(IndexError(...))
|
|
1372
|
+
>>> rz.non_empty([], error="Sequence is empty")
|
|
1373
|
+
Err('Sequence is empty')
|
|
1374
|
+
"""
|
|
1375
|
+
values = list(seq)
|
|
1376
|
+
if values:
|
|
1377
|
+
return values
|
|
1378
|
+
if error is MISSING:
|
|
1379
|
+
return Err(cast(Any, IndexError("Sequence is empty")))
|
|
1380
|
+
return Err(error)
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
@overload
|
|
1384
|
+
def single[T](seq: Iterable[T], /) -> Result[T, IndexError]: ...
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
@overload
|
|
1388
|
+
def single[T, E](seq: Iterable[T], /, error: E) -> Result[T, E]: ...
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
def single[T](seq: Iterable[T], /, error: Any = MISSING) -> Result[T, Any]:
|
|
1392
|
+
"""
|
|
1393
|
+
Return the single element in the given sequence, or an Error otherwise.
|
|
1394
|
+
|
|
1395
|
+
Args:
|
|
1396
|
+
seq: The sequence of values to check.
|
|
1397
|
+
error: The error to return if the sequence does not contain exactly one non-error value (defaults to IndexError).
|
|
1398
|
+
|
|
1399
|
+
Examples:
|
|
1400
|
+
>>> rz.single([42])
|
|
1401
|
+
42
|
|
1402
|
+
>>> rz.single([])
|
|
1403
|
+
Err(IndexError(...))
|
|
1404
|
+
>>> rz.single([1, 2])
|
|
1405
|
+
Err(IndexError(...))
|
|
1406
|
+
>>> rz.single([], error="No single value")
|
|
1407
|
+
Err('No single value')
|
|
1408
|
+
"""
|
|
1409
|
+
it = _iter(seq)
|
|
1410
|
+
try:
|
|
1411
|
+
value = next(it)
|
|
1412
|
+
except StopIteration:
|
|
1413
|
+
if error is MISSING:
|
|
1414
|
+
return Err(cast(Any, IndexError("Sequence is empty")))
|
|
1415
|
+
return Err(error)
|
|
1416
|
+
try:
|
|
1417
|
+
next(it)
|
|
1418
|
+
if error is MISSING:
|
|
1419
|
+
return Err(cast(Any, IndexError("Sequence contains more than one value")))
|
|
1420
|
+
return Err(error)
|
|
1421
|
+
except StopIteration:
|
|
1422
|
+
return value
|
|
@@ -8,6 +8,8 @@ import errorz as rz
|
|
|
8
8
|
|
|
9
9
|
EXCEPTION_LIST = [Exception, ValueError, TypeError, KeyError, IndexError]
|
|
10
10
|
|
|
11
|
+
__all__ = ["result", "exceptions", "errors", "error_messages"]
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
@st.composite
|
|
13
15
|
def result[T, E = Any](
|
|
@@ -17,8 +19,10 @@ def result[T, E = Any](
|
|
|
17
19
|
Generate Results with random values.
|
|
18
20
|
|
|
19
21
|
Args:
|
|
20
|
-
value:
|
|
21
|
-
|
|
22
|
+
value:
|
|
23
|
+
A strategy to generate the ok values.
|
|
24
|
+
error:
|
|
25
|
+
A strategy to generate the err values.
|
|
22
26
|
If None is given, it will generate random exceptions or primitive values as errors.
|
|
23
27
|
"""
|
|
24
28
|
|
|
@@ -43,8 +47,8 @@ def result[T, E = Any](
|
|
|
43
47
|
|
|
44
48
|
|
|
45
49
|
def exceptions(
|
|
46
|
-
exceptions: Iterable[type[
|
|
47
|
-
) -> st.SearchStrategy[
|
|
50
|
+
exceptions: Iterable[type[Exception]] | None = None,
|
|
51
|
+
) -> st.SearchStrategy[Exception]:
|
|
48
52
|
"""
|
|
49
53
|
Generate exceptions with random messages.
|
|
50
54
|
|
|
@@ -60,12 +64,26 @@ def exceptions(
|
|
|
60
64
|
return st.one_of([st.builds(exc, error_messages()) for exc in exceptions])
|
|
61
65
|
|
|
62
66
|
|
|
67
|
+
def errors[E = Exception](
|
|
68
|
+
error: st.SearchStrategy[E] | None = None,
|
|
69
|
+
) -> st.SearchStrategy[rz.Err[E]]:
|
|
70
|
+
"""
|
|
71
|
+
Generate random error values.
|
|
72
|
+
|
|
73
|
+
By default, it generates random exceptions with random messages.
|
|
74
|
+
"""
|
|
75
|
+
value = cast("st.SearchStrategy[E]", error or exceptions())
|
|
76
|
+
return value.map(rz.Err)
|
|
77
|
+
|
|
78
|
+
|
|
63
79
|
def error_messages() -> st.SearchStrategy[str]:
|
|
64
80
|
"""
|
|
65
81
|
Generate random error messages.
|
|
66
82
|
|
|
67
83
|
Those are small strings with common letters. We are not trying to stress
|
|
68
84
|
test the string handling capabilities of our error types.
|
|
85
|
+
|
|
86
|
+
If you want to stress test this, use the builtin st.text() strategy.
|
|
69
87
|
"""
|
|
70
88
|
return st.one_of(
|
|
71
89
|
st.sampled_from(["error", "fail", "invalid", "..."]),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|