errorz 0.1.0__tar.gz → 0.2.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.
- {errorz-0.1.0 → errorz-0.2.0}/PKG-INFO +1 -1
- {errorz-0.1.0 → errorz-0.2.0}/pyproject.toml +2 -1
- {errorz-0.1.0 → errorz-0.2.0}/src/errorz/__init__.py +162 -72
- {errorz-0.1.0 → errorz-0.2.0}/src/errorz/hypothesis.py +22 -4
- {errorz-0.1.0 → errorz-0.2.0}/README.md +0 -0
- {errorz-0.1.0 → errorz-0.2.0}/src/errorz/__main__.py +0 -0
- {errorz-0.1.0 → errorz-0.2.0}/src/errorz/mypy.py +0 -0
- {errorz-0.1.0 → errorz-0.2.0}/src/errorz/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "errorz"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
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,6 +24,8 @@ 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
|
|
29
31
|
from types import NotImplementedType
|
|
@@ -31,7 +33,10 @@ from typing import (
|
|
|
31
33
|
Any,
|
|
32
34
|
Callable,
|
|
33
35
|
Generic,
|
|
36
|
+
Hashable,
|
|
34
37
|
Iterable,
|
|
38
|
+
Literal,
|
|
39
|
+
Mapping,
|
|
35
40
|
Optional,
|
|
36
41
|
Protocol,
|
|
37
42
|
Self,
|
|
@@ -56,9 +61,9 @@ __all__ = (
|
|
|
56
61
|
"expect",
|
|
57
62
|
"map",
|
|
58
63
|
"coalesce",
|
|
59
|
-
"
|
|
64
|
+
"filter",
|
|
60
65
|
"zip",
|
|
61
|
-
"
|
|
66
|
+
"some",
|
|
62
67
|
"getattr",
|
|
63
68
|
"call",
|
|
64
69
|
"iter",
|
|
@@ -73,7 +78,6 @@ T_co = TypeVar("T_co", covariant=True)
|
|
|
73
78
|
type Result[T, E = Any] = T | Err[E]
|
|
74
79
|
|
|
75
80
|
|
|
76
|
-
@final
|
|
77
81
|
class Err(Generic[E_co]):
|
|
78
82
|
"""
|
|
79
83
|
A simple error wrapper that implements the Error protocol. It is used to wrap
|
|
@@ -92,10 +96,14 @@ class Err(Generic[E_co]):
|
|
|
92
96
|
def __repr__(self) -> str:
|
|
93
97
|
return f"Err({self._error!r})"
|
|
94
98
|
|
|
99
|
+
def __hash__[E: Hashable](self: Err[E]) -> int:
|
|
100
|
+
return hash(self._error)
|
|
101
|
+
|
|
95
102
|
#
|
|
96
103
|
# Python magic methods
|
|
97
104
|
#
|
|
98
|
-
|
|
105
|
+
@final
|
|
106
|
+
def __bool__(self) -> Literal[False]:
|
|
99
107
|
return False
|
|
100
108
|
|
|
101
109
|
# Arithmetic operations
|
|
@@ -211,6 +219,7 @@ def _any(x: object) -> Any:
|
|
|
211
219
|
return x
|
|
212
220
|
|
|
213
221
|
|
|
222
|
+
_iter = builtins.iter
|
|
214
223
|
_getattr = builtins.getattr
|
|
215
224
|
|
|
216
225
|
|
|
@@ -283,6 +292,10 @@ def is_err(value: Result[Any], /) -> TypeIs[Err]:
|
|
|
283
292
|
False
|
|
284
293
|
|
|
285
294
|
Notes:
|
|
295
|
+
The corresponding is_ok() does not exist because typecheckers often struggle
|
|
296
|
+
with type narrowing because Python type system do not include negative
|
|
297
|
+
bounds (e.g. "T but not Err").
|
|
298
|
+
|
|
286
299
|
The Err case evaluates to False. If you known that T is never falsy,
|
|
287
300
|
you can just use the common Pythonic ways of checking for nullable
|
|
288
301
|
values:
|
|
@@ -298,25 +311,9 @@ def is_err(value: Result[Any], /) -> TypeIs[Err]:
|
|
|
298
311
|
return isinstance(value, Err)
|
|
299
312
|
|
|
300
313
|
|
|
301
|
-
def
|
|
302
|
-
"""
|
|
303
|
-
Check if result is in the Ok case.
|
|
304
|
-
|
|
305
|
-
Args:
|
|
306
|
-
value: The result value to check.
|
|
307
|
-
|
|
308
|
-
Examples:
|
|
309
|
-
>>> rz.is_ok(rz.err('error'))
|
|
310
|
-
False
|
|
311
|
-
>>> rz.is_ok(42)
|
|
312
|
-
True
|
|
313
|
-
"""
|
|
314
|
-
return not isinstance(value, Err)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def validate[T](value: Result[T], /) -> TypeIs[T]:
|
|
314
|
+
def check[T](value: Result[T], /) -> TypeIs[T]:
|
|
318
315
|
"""
|
|
319
|
-
|
|
316
|
+
Check if value is not an Err and return True.
|
|
320
317
|
|
|
321
318
|
If the value is an error, raise it or a UnwrapError if E is not an exception.
|
|
322
319
|
|
|
@@ -648,22 +645,6 @@ def coalesce(*values: Any) -> Any:
|
|
|
648
645
|
return err
|
|
649
646
|
|
|
650
647
|
|
|
651
|
-
def values[T, E](seq: Iterable[Result[T, E]], /) -> Iterable[T]:
|
|
652
|
-
"""
|
|
653
|
-
Return an iterable of the non-error values in the given sequence.
|
|
654
|
-
|
|
655
|
-
Args:
|
|
656
|
-
seq: The sequence of options to filter.
|
|
657
|
-
|
|
658
|
-
Examples:
|
|
659
|
-
>>> list(rz.values([1, 2, rz.err('error'), 3]))
|
|
660
|
-
[1, 2, 3]
|
|
661
|
-
>>> list(rz.values([rz.err("error 1"), rz.err("error 2")]))
|
|
662
|
-
[]
|
|
663
|
-
"""
|
|
664
|
-
return (x for x in seq if not isinstance(x, Err))
|
|
665
|
-
|
|
666
|
-
|
|
667
648
|
@overload
|
|
668
649
|
def zip[T1, T2, E](
|
|
669
650
|
x1: Result[T1, E], x2: Result[T2, E], /
|
|
@@ -761,40 +742,6 @@ def zip(*args: Any) -> Any:
|
|
|
761
742
|
return args
|
|
762
743
|
|
|
763
744
|
|
|
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
745
|
class Caller[**P, R](Protocol):
|
|
799
746
|
def __call__(
|
|
800
747
|
self, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
|
|
@@ -1001,3 +948,146 @@ def _wrap_safe[F: Callable[..., Any]](fn: Any, impl: F) -> F:
|
|
|
1001
948
|
if ret_type is not None:
|
|
1002
949
|
impl.__annotations__["return"] = Result[ret_type, Exception] # type: ignore
|
|
1003
950
|
return impl
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
#
|
|
954
|
+
# Lists, iterators, and other collections
|
|
955
|
+
#
|
|
956
|
+
def some[T, E](value: Result[T, E]) -> Iterable[T]:
|
|
957
|
+
"""
|
|
958
|
+
Yield nothing or the ok value, if it exists.
|
|
959
|
+
|
|
960
|
+
Args:
|
|
961
|
+
value: The fallible value to yield.
|
|
962
|
+
|
|
963
|
+
Examples:
|
|
964
|
+
>>> list(rz.some(42))
|
|
965
|
+
[42]s
|
|
966
|
+
>>> list(rz.some(rz.err('error')))
|
|
967
|
+
[]
|
|
968
|
+
"""
|
|
969
|
+
if not isinstance(value, Err):
|
|
970
|
+
yield value
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def iter[T, E](value: Result[Iterable[T], E]) -> Iterable[T]:
|
|
974
|
+
"""
|
|
975
|
+
Yield nothing or the elements of an iterable.
|
|
976
|
+
|
|
977
|
+
Args:
|
|
978
|
+
value: The fallible iterable to yield from.
|
|
979
|
+
|
|
980
|
+
Examples:
|
|
981
|
+
>>> list(rz.iter([42]))
|
|
982
|
+
[42]
|
|
983
|
+
>>> list(rz.iter(rz.err('error')))
|
|
984
|
+
[]
|
|
985
|
+
"""
|
|
986
|
+
if not isinstance(value, Err):
|
|
987
|
+
yield from value
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def filter[T, E](seq: Iterable[Result[T, E]], /) -> Iterable[T]:
|
|
991
|
+
"""
|
|
992
|
+
Return an iterable of the non-error values in the given sequence.
|
|
993
|
+
|
|
994
|
+
Args:
|
|
995
|
+
seq: The sequence of fallible values to filter.
|
|
996
|
+
|
|
997
|
+
Examples:
|
|
998
|
+
>>> list(rz.values([1, 2, rz.err('error'), 3]))
|
|
999
|
+
[1, 2, 3]
|
|
1000
|
+
>>> list(rz.values([rz.err("error 1"), rz.err("error 2")]))
|
|
1001
|
+
[]
|
|
1002
|
+
"""
|
|
1003
|
+
return (x for x in seq if not isinstance(x, Err))
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def filter_values[K, V, E](seq: Mapping[K, Result[V, E]], /) -> dict[K, V]:
|
|
1007
|
+
"""
|
|
1008
|
+
Remove all Err values from the given mapping.
|
|
1009
|
+
|
|
1010
|
+
Args:
|
|
1011
|
+
seq: The mapping of fallible values to filter.
|
|
1012
|
+
|
|
1013
|
+
Examples:
|
|
1014
|
+
>>> rz.filter_values({'a': 1, 'b': rz.err('error'), 'c': 3})
|
|
1015
|
+
{'a': 1, 'c': 3}
|
|
1016
|
+
>>> rz.filter_values({'a': rz.err("error 1"), 'b': rz.err("error 2")})
|
|
1017
|
+
{}
|
|
1018
|
+
"""
|
|
1019
|
+
return {k: v for k, v in seq.items() if not isinstance(v, Err)}
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
@overload
|
|
1023
|
+
def non_empty[T](seq: Iterable[T], /) -> Result[list[T], IndexError]: ...
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
@overload
|
|
1027
|
+
def non_empty[T, E](seq: Iterable[T], /, error: E) -> Result[list[T], E]: ...
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def non_empty[T](seq: Iterable[T], /, error: Any = MISSING) -> Result[list[T], Any]:
|
|
1031
|
+
"""
|
|
1032
|
+
Return the given sequence if it contains at least one non-error value, or an Error otherwise.
|
|
1033
|
+
|
|
1034
|
+
Args:
|
|
1035
|
+
seq: The sequence of values to check.
|
|
1036
|
+
error: The error to return if the sequence is empty (defaults to IndexError).
|
|
1037
|
+
|
|
1038
|
+
Examples:
|
|
1039
|
+
>>> rz.non_empty([1, 2, 3])
|
|
1040
|
+
[1, 2, 3]
|
|
1041
|
+
>>> rz.non_empty([])
|
|
1042
|
+
Err(IndexError())
|
|
1043
|
+
>>> rz.non_empty([], error="Sequence is empty")
|
|
1044
|
+
Err('Sequence is empty')
|
|
1045
|
+
"""
|
|
1046
|
+
values = list(seq)
|
|
1047
|
+
if values:
|
|
1048
|
+
return values
|
|
1049
|
+
if error is MISSING:
|
|
1050
|
+
return Err(cast(Any, IndexError("Sequence is empty")))
|
|
1051
|
+
return Err(error)
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
@overload
|
|
1055
|
+
def single[T](seq: Iterable[T], /) -> Result[T, IndexError]: ...
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
@overload
|
|
1059
|
+
def single[T, E](seq: Iterable[T], /, error: E) -> Result[T, E]: ...
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def single[T](seq: Iterable[T], /, error: Any = MISSING) -> Result[T, Any]:
|
|
1063
|
+
"""
|
|
1064
|
+
Return the single element in the given sequence, or an Error otherwise.
|
|
1065
|
+
|
|
1066
|
+
Args:
|
|
1067
|
+
seq: The sequence of values to check.
|
|
1068
|
+
error: The error to return if the sequence does not contain exactly one non-error value (defaults to IndexError).
|
|
1069
|
+
|
|
1070
|
+
Examples:
|
|
1071
|
+
>>> rz.single([42])
|
|
1072
|
+
42
|
|
1073
|
+
>>> rz.single([])
|
|
1074
|
+
Err(ValueError())
|
|
1075
|
+
>>> rz.single([1, 2])
|
|
1076
|
+
Err(ValueError())
|
|
1077
|
+
>>> rz.single([], error="No single value")
|
|
1078
|
+
Err('No single value')
|
|
1079
|
+
"""
|
|
1080
|
+
it = _iter(seq)
|
|
1081
|
+
try:
|
|
1082
|
+
value = next(it)
|
|
1083
|
+
except StopIteration:
|
|
1084
|
+
if error is MISSING:
|
|
1085
|
+
return Err(cast(Any, IndexError("Sequence is empty")))
|
|
1086
|
+
return Err(error)
|
|
1087
|
+
try:
|
|
1088
|
+
next(it)
|
|
1089
|
+
if error is MISSING:
|
|
1090
|
+
return Err(cast(Any, IndexError("Sequence contains more than one value")))
|
|
1091
|
+
return Err(error)
|
|
1092
|
+
except StopIteration:
|
|
1093
|
+
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
|
|
File without changes
|