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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: errorz
3
- Version: 0.1.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.1.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 = [
@@ -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
- "values",
67
+ "filter",
60
68
  "zip",
61
- "elements",
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
- def __bool__(self) -> bool:
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 is_ok[T](value: Result[T], /) -> TypeIs[T]:
327
+ def check[T](value: Result[T], /) -> TypeIs[T]:
302
328
  """
303
- Check if result is in the Ok case.
329
+ Check if value is not an Err and return True.
304
330
 
305
- Args:
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
- Examples:
309
- >>> rz.is_ok(rz.err('error'))
310
- False
311
- >>> rz.is_ok(42)
312
- True
333
+ Args:
334
+ value: The result value to validate.
313
335
  """
314
- return not isinstance(value, Err)
336
+ if isinstance(value, Err):
337
+ raise value.exception()
338
+ return True
315
339
 
316
340
 
317
- def validate[T](value: Result[T], /) -> TypeIs[T]:
341
+ def tagged[T, E](value: Result[T, E], /) -> IsOk[T] | IsErr[E]:
318
342
  """
319
- Accept the Ok and return True.
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 validate.
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
- raise value.exception()
328
- return True
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 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.
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('e1')
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.values`
782
+ - :func:`rz.all`
636
783
  """
637
784
  if len(values) == 1:
638
785
  values = values[0]
639
786
 
640
- err: Err[Any] | None = None
787
+ value: Any = None
641
788
  for value in values:
642
789
  if not isinstance(value, Err):
643
790
  return value
644
- elif err is None:
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 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]: ...
649
839
 
650
840
 
651
- def values[T, E](seq: Iterable[Result[T, E]], /) -> Iterable[T]:
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 an iterable of the non-error values in the given sequence.
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
- seq: The sequence of options to filter.
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
- >>> 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
- []
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
- return (x for x in seq if not isinstance(x, Err))
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
@@ -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
+ )
@@ -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", "..."]),
@@ -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