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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: errorz
3
- Version: 0.1.0
3
+ Version: 0.2.0
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.1.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
- "values",
64
+ "filter",
60
65
  "zip",
61
- "elements",
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
- def __bool__(self) -> bool:
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 is_ok[T](value: Result[T], /) -> TypeIs[T]:
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
- Accept the Ok and return True.
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: A strategy to generate the ok values.
21
- error: A strategy to generate the err values.
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[BaseException]] | None = None,
47
- ) -> st.SearchStrategy[BaseException]:
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