errorz 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,1003 @@
1
+ """
2
+ Copyright (c) 2026, Fábio Macêdo Mendes
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+
22
+ This is the main errorz module. It is implemented in a single Python file to
23
+ help vendorizing it. It is distributed as a package, instead of a errorz.py file,
24
+ to allow for py.typed marker.
25
+ """
26
+
27
+ import builtins
28
+ import functools
29
+ from types import NotImplementedType
30
+ from typing import (
31
+ Any,
32
+ Callable,
33
+ Generic,
34
+ Iterable,
35
+ Optional,
36
+ Protocol,
37
+ Self,
38
+ TypeIs,
39
+ TypeVar,
40
+ cast,
41
+ final,
42
+ overload,
43
+ )
44
+
45
+ # Raised when trying to unwrap a None value. It is an alias for ValueError to allow
46
+ # mocking it in tests (or global mokey-patching if you are feelling adventurous).
47
+ UnwrapError = ValueError
48
+
49
+ __version__ = "0.1.0"
50
+ __author__ = "Fábio Macêdo Mendes"
51
+ __all__ = (
52
+ "Result",
53
+ "UnwrapError",
54
+ "unwrap",
55
+ "unwrap_lazy",
56
+ "expect",
57
+ "map",
58
+ "coalesce",
59
+ "values",
60
+ "zip",
61
+ "elements",
62
+ "getattr",
63
+ "call",
64
+ "iter",
65
+ )
66
+
67
+ MISSING: NotImplementedType = cast(NotImplementedType, object())
68
+
69
+ E_co = TypeVar("E_co", covariant=True, default=Any, bound=object)
70
+ T_co = TypeVar("T_co", covariant=True)
71
+
72
+ # type Result[T_co, E_co = Any] = Err[E_co] | T_co
73
+ type Result[T, E = Any] = T | Err[E]
74
+
75
+
76
+ @final
77
+ class Err(Generic[E_co]):
78
+ """
79
+ A simple error wrapper that implements the Error protocol. It is used to wrap
80
+ error values in a way that they can be distinguished from regular values.
81
+ """
82
+
83
+ __is_errorz_error__ = True
84
+
85
+ @property
86
+ def error(self) -> E_co:
87
+ return self._error
88
+
89
+ def __init__(self, error: E_co) -> None:
90
+ self._error = error
91
+
92
+ def __repr__(self) -> str:
93
+ return f"Err({self._error!r})"
94
+
95
+ #
96
+ # Python magic methods
97
+ #
98
+ def __bool__(self) -> bool:
99
+ return False
100
+
101
+ # Arithmetic operations
102
+ def __add__(self, other: Any) -> Self:
103
+ return self
104
+
105
+ def __radd__(self, other: Any) -> Self:
106
+ return self
107
+
108
+ def __mul__(self, other: Any) -> Self:
109
+ return self
110
+
111
+ def __rmul__(self, other: Any) -> Self:
112
+ return self
113
+
114
+ def __matmul__(self, other: Any) -> Self:
115
+ return self
116
+
117
+ def __rmatmul__(self, other: Any) -> Self:
118
+ return self
119
+
120
+ def __sub__(self, other: Any) -> Self:
121
+ return self
122
+
123
+ def __rsub__(self, other: Any) -> Self:
124
+ return self
125
+
126
+ def __truediv__(self, other: Any) -> Self:
127
+ return self
128
+
129
+ def __rtruediv__(self, other: Any) -> Self:
130
+ return self
131
+
132
+ def __floordiv__(self, other: Any) -> Self:
133
+ return self
134
+
135
+ def __rfloordiv__(self, other: Any) -> Self:
136
+ return self
137
+
138
+ def __mod__(self, other: Any) -> Self:
139
+ return self
140
+
141
+ def __rmod__(self, other: Any) -> Self:
142
+ return self
143
+
144
+ def __pow__(self, other: Any) -> Self:
145
+ return self
146
+
147
+ def __rpow__(self, other: Any) -> Self:
148
+ return self
149
+
150
+ def __or__(self, other: Any) -> Self:
151
+ return self
152
+
153
+ def __ror__(self, other: Any) -> Self:
154
+ return self
155
+
156
+ def __and__(self, other: Any) -> Self:
157
+ return self
158
+
159
+ def __rand__(self, other: Any) -> Self:
160
+ return self
161
+
162
+ def __xor__(self, other: Any) -> Self:
163
+ return self
164
+
165
+ def __rxor__(self, other: Any) -> Self:
166
+ return self
167
+
168
+ def __rshift__(self, other: Any) -> Self:
169
+ return self
170
+
171
+ def __rrshift__(self, other: Any) -> Self:
172
+ return self
173
+
174
+ def __lshift__(self, other: Any) -> Self:
175
+ return self
176
+
177
+ def __rlshift__(self, other: Any) -> Self:
178
+ return self
179
+
180
+ def __gt__(self, other: Any) -> Self:
181
+ return self
182
+
183
+ def __ge__(self, other: Any) -> Self:
184
+ return self
185
+
186
+ def __le__(self, other: Any) -> Self:
187
+ return self
188
+
189
+ def __lt__(self, other: Any) -> Self:
190
+ return self
191
+
192
+ #
193
+ # Public methods
194
+ #
195
+ def exception(self) -> BaseException:
196
+ """
197
+ Return the error as an exception.
198
+
199
+ Non-exceptional values are converted to a UnwrapError with self.error
200
+ as an argument.
201
+ """
202
+ e = self._error
203
+ if isinstance(e, BaseException):
204
+ return e
205
+ if isinstance(e, type) and issubclass(e, BaseException):
206
+ return UnwrapError(e)
207
+ return UnwrapError(e)
208
+
209
+
210
+ def _any(x: object) -> Any:
211
+ return x
212
+
213
+
214
+ _getattr = builtins.getattr
215
+
216
+
217
+ #
218
+ # Constructors
219
+ #
220
+ @overload
221
+ def err[E](error: E, /) -> Result[Any, E]: ...
222
+
223
+
224
+ @overload
225
+ def err[T, E](error: E, /, type: type[T]) -> Result[T, E]: ...
226
+
227
+
228
+ def err(error: Any, /, type: Any = None) -> Any:
229
+ """
230
+ Creates an error value.
231
+
232
+ Args:
233
+ error: The error value to wrap.
234
+
235
+ Examples:
236
+ >>> rz.err('error') # type is Result[Any, str]
237
+ Err('error')
238
+ >>> rz.err('error', type=int) # type is Result[int, str]
239
+ Err('error')
240
+ """
241
+ return Err(error)
242
+
243
+
244
+ @overload
245
+ def ok[T](value: T, /) -> Result[T, Any]: ...
246
+
247
+
248
+ @overload
249
+ def ok[T, E](value: T, /, err: type[E]) -> Result[T, E]: ...
250
+
251
+
252
+ def ok[T, E](value: Any, /, err: Any = None) -> Any:
253
+ """
254
+ Create a success value. This is just an identity function, but it can be used
255
+ for symmetry with `err` and to make it clear that a value is intended to be a
256
+ success value.
257
+
258
+ You can also pass the type of the error case to appease the type checker.
259
+
260
+ Args:
261
+ value: The success value to return.
262
+
263
+ Examples:
264
+ >>> rz.ok(42) # type is Result[int, Any]
265
+ 42
266
+ >>> rz.ok(42, err=str) # type is Result[int, str]
267
+ 42
268
+ """
269
+ return value
270
+
271
+
272
+ def is_err(value: Result[Any], /) -> TypeIs[Err]:
273
+ """
274
+ Check if the value is an error.
275
+
276
+ Args:
277
+ value: The result value to check.
278
+
279
+ Examples:
280
+ >>> rz.is_err(rz.err('error'))
281
+ True
282
+ >>> rz.is_err(42)
283
+ False
284
+
285
+ Notes:
286
+ The Err case evaluates to False. If you known that T is never falsy,
287
+ you can just use the common Pythonic ways of checking for nullable
288
+ values:
289
+
290
+ >>> value = rz.err(42)
291
+ >>> print(value or "error")
292
+ error
293
+ >>> if value:
294
+ ... print("This will never be an error case!")
295
+
296
+ The same caveats of Optional types apply here.
297
+ """
298
+ return isinstance(value, Err)
299
+
300
+
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]:
318
+ """
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.
322
+
323
+ Args:
324
+ value: The result value to validate.
325
+ """
326
+ if isinstance(value, Err):
327
+ raise value.exception()
328
+ return True
329
+
330
+
331
+ #
332
+ # Unwrappers
333
+ #
334
+ def unwrap[T](value: Result[T], /, default: T = MISSING) -> T:
335
+ """
336
+ Unwrap a result.
337
+
338
+ If the value is an error and no default is provided, raise the
339
+ :meth:`Err.exception` method associated with it.
340
+
341
+ Args:
342
+ value: The result value to unwrap.
343
+
344
+ Examples:
345
+ >>> rz.unwrap(42)
346
+ 42
347
+ >>> rz.unwrap(rz.err('error'))
348
+ Traceback (most recent call last):
349
+ ...
350
+ ValueError: error
351
+
352
+ See also:
353
+ - :func:`rz.expect`
354
+ - :func:`rz.unwrap_lazy`
355
+ """
356
+ if isinstance(value, Err):
357
+ if default is MISSING:
358
+ raise value.exception()
359
+ return default
360
+ return value
361
+
362
+
363
+ def unwrap_lazy[T](value: Result[T], /, default: Callable[[], T]) -> T:
364
+ """
365
+ Unwrap a value that may be None. If the value is None, return the result of
366
+ calling the default function.
367
+
368
+ This may be used instead of :func:`rz.unwrap` when the default value is
369
+ expensive to compute or produces side effects.
370
+
371
+ Args:
372
+ value: The optional value to unwrap.
373
+ default: The function to call if the optional value is None.
374
+
375
+ Examples:
376
+ >>> rz.unwrap_lazy(0, lambda: 42)
377
+ 0
378
+ >>> rz.unwrap_lazy(rz.err('error'), lambda: 42)
379
+ 42
380
+
381
+ See also:
382
+ - :func:`rz.unwrap`
383
+ """
384
+ if isinstance(value, Err):
385
+ if default is MISSING:
386
+ raise value.exception()
387
+ return default()
388
+ return value
389
+
390
+
391
+ def expect[T](value: Result[T], /, error: str | BaseException) -> T:
392
+ """
393
+ Unwrap a value that may be None. If the value is None, raise the provided
394
+ error or raises an UnwrapError (ValueError, usually) if a string is
395
+ provided instead of an exception.
396
+
397
+ Args:
398
+ value: The optional value to unwrap.
399
+ error: The error to raise if the value is None.
400
+
401
+ Examples:
402
+ >>> rz.expect(42, "Value is None")
403
+ 42
404
+ >>> rz.expect(rz.err(ZeroDivisionError()), "Value is an error")
405
+ Traceback (most recent call last):
406
+ ...
407
+ ValueError: Value is an error
408
+
409
+ See also:
410
+ - :func:`rz.unwrap`
411
+ """
412
+ if not isinstance(value, Err):
413
+ return value
414
+ elif isinstance(error, BaseException):
415
+ raise error
416
+ raise UnwrapError(error)
417
+
418
+
419
+ def to_optional[T](value: Result[T, Any]) -> Optional[T]:
420
+ """
421
+ Convert a Result to an Optional by converting any error to None.
422
+
423
+ Args:
424
+ value: The result value to convert.
425
+
426
+ Returns:
427
+ An Optional containing the unwrapped value if it is Ok, or None if it
428
+ is an Err.
429
+ """
430
+ if isinstance(value, Err):
431
+ return None
432
+ return value
433
+
434
+
435
+ @overload
436
+ def map[T, E, R](fn: Callable[[T], R], value: Result[T, E], /) -> Result[R, E]: ...
437
+
438
+
439
+ @overload
440
+ def map[T1, T2, E, R](
441
+ fn: Callable[[T1, T2], R], x1: Result[T1, E], x2: Result[T2, E], /
442
+ ) -> Result[R, E]: ...
443
+
444
+
445
+ @overload
446
+ def map[T1, T2, T3, E, R](
447
+ fn: Callable[[T1, T2, T3], R],
448
+ x1: Result[T1, E],
449
+ x2: Result[T2, E],
450
+ x3: Result[T3, E],
451
+ /,
452
+ ) -> Result[R, E]: ...
453
+
454
+
455
+ @overload
456
+ def map[T1, T2, T3, T4, E, R](
457
+ fn: Callable[[T1, T2, T3, T4], R],
458
+ x1: Result[T1, E],
459
+ x2: Result[T2, E],
460
+ x3: Result[T3, E],
461
+ x4: Result[T4, E],
462
+ /,
463
+ ) -> Result[R, E]: ...
464
+
465
+
466
+ @overload
467
+ def map[T1, T2, T3, T4, T5, E, R](
468
+ fn: Callable[[T1, T2, T3, T4, T5], R],
469
+ x1: Result[T1, E],
470
+ x2: Result[T2, E],
471
+ x3: Result[T3, E],
472
+ x4: Result[T4, E],
473
+ x5: Result[T5, E],
474
+ /,
475
+ ) -> Result[R, E]: ...
476
+
477
+
478
+ @overload
479
+ def map[T1, T2, T3, T4, T5, T6, E, R](
480
+ fn: Callable[[T1, T2, T3, T4, T5, T6], R],
481
+ x1: Result[T1, E],
482
+ x2: Result[T2, E],
483
+ x3: Result[T3, E],
484
+ x4: Result[T4, E],
485
+ x5: Result[T5, E],
486
+ x6: Result[T6, E],
487
+ /,
488
+ ) -> Result[R, E]: ...
489
+
490
+
491
+ @overload
492
+ def map[T1, T2, T3, T4, T5, T6, T7, E, R](
493
+ fn: Callable[[T1, T2, T3, T4, T5, T6, T7], R],
494
+ x1: Result[T1, E],
495
+ x2: Result[T2, E],
496
+ x3: Result[T3, E],
497
+ x4: Result[T4, E],
498
+ x5: Result[T5, E],
499
+ x6: Result[T6, E],
500
+ x7: Result[T7, E],
501
+ /,
502
+ ) -> Result[R, E]: ...
503
+
504
+
505
+ @overload
506
+ def map[T1, T2, T3, T4, T5, T6, T7, T8, E, R](
507
+ fn: Callable[[T1, T2, T3, T4, T5, T6, T7, T8], R],
508
+ x1: Result[T1, E],
509
+ x2: Result[T2, E],
510
+ x3: Result[T3, E],
511
+ x4: Result[T4, E],
512
+ x5: Result[T5, E],
513
+ x6: Result[T6, E],
514
+ x7: Result[T7, E],
515
+ x8: Result[T8, E],
516
+ /,
517
+ ) -> Result[R, E]: ...
518
+
519
+
520
+ def map(fn: Any, *values: Any) -> Any:
521
+ """
522
+ Apply the function to the values if none of them are errors, otherwise
523
+ return the first error found.
524
+
525
+
526
+ Args:
527
+ fn:
528
+ The function to apply to the values if none of them are errors.
529
+ x1, x2, ...:
530
+ The result values to apply the function to.
531
+ It accepts any number of arguments (but only type checks up to 8).
532
+
533
+ Examples:
534
+ >>> rz.map(lambda x: x + 1, 41)
535
+ 42
536
+ >>> rz.map(lambda x, y: x + y, 40, 2)
537
+ 42
538
+ >>> rz.map(lambda x, y: x + y, 42, rz.err('error'))
539
+ Err('error')
540
+
541
+ See also:
542
+ - :func:`rz.zip`
543
+ """
544
+ if any(is_err(value) for value in values):
545
+ return next(value for value in values if is_err(value))
546
+ return fn(*values)
547
+
548
+
549
+ @overload
550
+ def coalesce[T, E](values: Iterable[Result[T, E]], /) -> Result[T, E]: ...
551
+
552
+
553
+ @overload
554
+ def coalesce[T1, T2, E1, E2](
555
+ x1: Result[T1, E1], x2: Result[T2, E2], /
556
+ ) -> Result[T1 | T2, E1 | E2]: ...
557
+
558
+
559
+ @overload
560
+ def coalesce[T1, T2, T3, E](
561
+ x1: Result[T1, E], x2: Result[T2, E], x3: Result[T3, E], /
562
+ ) -> Result[T1 | T2 | T3, E]: ...
563
+
564
+
565
+ @overload
566
+ def coalesce[T1, T2, T3, T4, E](
567
+ x1: Result[T1, E], x2: Result[T2, E], x3: Result[T3, E], x4: Result[T4, E], /
568
+ ) -> Result[T1 | T2 | T3 | T4, E]: ...
569
+
570
+
571
+ @overload
572
+ def coalesce[T1, T2, T3, T4, T5, E](
573
+ x1: Result[T1, E],
574
+ x2: Result[T2, E],
575
+ x3: Result[T3, E],
576
+ x4: Result[T4, E],
577
+ x5: Result[T5, E],
578
+ /,
579
+ ) -> Result[T1 | T2 | T3 | T4 | T5, E]: ...
580
+
581
+
582
+ @overload
583
+ def coalesce[T1, T2, T3, T4, T5, T6, E](
584
+ x1: Result[T1, E],
585
+ x2: Result[T2, E],
586
+ x3: Result[T3, E],
587
+ x4: Result[T4, E],
588
+ x5: Result[T5, E],
589
+ x6: Result[T6, E],
590
+ /,
591
+ ) -> Result[T1 | T2 | T3 | T4 | T5 | T6, E]: ...
592
+
593
+
594
+ @overload
595
+ def coalesce[T1, T2, T3, T4, T5, T6, T7, E](
596
+ x1: Result[T1, E],
597
+ x2: Result[T2, E],
598
+ x3: Result[T3, E],
599
+ x4: Result[T4, E],
600
+ x5: Result[T5, E],
601
+ x6: Result[T6, E],
602
+ x7: Result[T7, E],
603
+ /,
604
+ ) -> Result[T1 | T2 | T3 | T4 | T5 | T6 | T7, E]: ...
605
+
606
+
607
+ @overload
608
+ def coalesce[T1, T2, T3, T4, T5, T6, T7, T8, E](
609
+ x1: Result[T1, E],
610
+ x2: Result[T2, E],
611
+ x3: Result[T3, E],
612
+ x4: Result[T4, E],
613
+ x5: Result[T5, E],
614
+ x6: Result[T6, E],
615
+ x7: Result[T7, E],
616
+ x8: Result[T8, E],
617
+ /,
618
+ ) -> Result[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8, E]: ...
619
+
620
+
621
+ def coalesce(*values: Any) -> Any:
622
+ """
623
+ Return the first value that is not an error, or the first error if all values are errors.
624
+
625
+ Examples:
626
+ >>> rz.coalesce(rz.err("e1"), rz.err("e2"), 42, 43, ...)
627
+ 42
628
+ >>> rz.coalesce(rz.err("e1"), rz.err("e2"))
629
+ Err('e1')
630
+ >>> rz.coalesce([rz.err("e1"), rz.err("e2"), 42, 43, ...])
631
+ 42
632
+
633
+ See also:
634
+ - :func:`rz.zip`
635
+ - :func:`rz.values`
636
+ """
637
+ if len(values) == 1:
638
+ values = values[0]
639
+
640
+ err: Err[Any] | None = None
641
+ for value in values:
642
+ if not isinstance(value, Err):
643
+ return value
644
+ elif err is None:
645
+ err = value
646
+ if err is None:
647
+ raise ValueError("coalesce() expected at least one value")
648
+ return err
649
+
650
+
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
+ @overload
668
+ def zip[T1, T2, E](
669
+ x1: Result[T1, E], x2: Result[T2, E], /
670
+ ) -> Result[tuple[T1, T2], E]: ...
671
+
672
+
673
+ @overload
674
+ def zip[T1, T2, T3, E](
675
+ x1: Result[T1, E],
676
+ x2: Result[T2, E],
677
+ x3: Result[T3, E],
678
+ /,
679
+ ) -> Result[tuple[T1, T2, T3], E]: ...
680
+
681
+
682
+ @overload
683
+ def zip[T1, T2, T3, T4, E](
684
+ x1: Result[T1, E],
685
+ x2: Result[T2, E],
686
+ x3: Result[T3, E],
687
+ x4: Result[T4, E],
688
+ /,
689
+ ) -> Result[tuple[T1, T2, T3, T4], E]: ...
690
+
691
+
692
+ @overload
693
+ def zip[T1, T2, T3, T4, T5, E](
694
+ x1: Result[T1, E],
695
+ x2: Result[T2, E],
696
+ x3: Result[T3, E],
697
+ x4: Result[T4, E],
698
+ x5: Result[T5, E],
699
+ /,
700
+ ) -> Result[tuple[T1, T2, T3, T4, T5], E]: ...
701
+
702
+
703
+ @overload
704
+ def zip[T1, T2, T3, T4, T5, T6, E](
705
+ x1: Result[T1, E],
706
+ x2: Result[T2, E],
707
+ x3: Result[T3, E],
708
+ x4: Result[T4, E],
709
+ x5: Result[T5, E],
710
+ x6: Result[T6, E],
711
+ /,
712
+ ) -> Result[tuple[T1, T2, T3, T4, T5, T6], E]: ...
713
+
714
+
715
+ @overload
716
+ def zip[T1, T2, T3, T4, T5, T6, T7, E](
717
+ x1: Result[T1, E],
718
+ x2: Result[T2, E],
719
+ x3: Result[T3, E],
720
+ x4: Result[T4, E],
721
+ x5: Result[T5, E],
722
+ x6: Result[T6, E],
723
+ x7: Result[T7, E],
724
+ /,
725
+ ) -> Result[tuple[T1, T2, T3, T4, T5, T6, T7], E]: ...
726
+
727
+
728
+ @overload
729
+ def zip[T1, T2, T3, T4, T5, T6, T7, T8, E](
730
+ x1: Result[T1, E],
731
+ x2: Result[T2, E],
732
+ x3: Result[T3, E],
733
+ x4: Result[T4, E],
734
+ x5: Result[T5, E],
735
+ x6: Result[T6, E],
736
+ x7: Result[T7, E],
737
+ x8: Result[T8, E],
738
+ /,
739
+ ) -> Result[tuple[T1, T2, T3, T4, T5, T6, T7, T8], E]: ...
740
+
741
+
742
+ def zip(*args: Any) -> Any:
743
+ """
744
+ Return a tuple if no argument is an error and the first error otherwise.
745
+
746
+ Args:
747
+ x1, x2, ...:
748
+ The optional values to check.
749
+ It accepts any number of arguments (but only type checks up to 8).
750
+
751
+ Examples:
752
+ >>> rz.zip(1, 2, 3)
753
+ (1, 2, 3)
754
+ >>> rz.zip(1, 2, rz.err('error'))
755
+ Err('error')
756
+
757
+ """
758
+ for arg in args:
759
+ if isinstance(arg, Err):
760
+ return arg
761
+ return args
762
+
763
+
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
+ class Caller[**P, R](Protocol):
799
+ def __call__(
800
+ self, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
801
+ ) -> R: ...
802
+
803
+
804
+ class _CallFactory:
805
+ """
806
+ Call the optional function with the given arguments or return the error.
807
+
808
+ Args:
809
+ fn: The fallible function to call.
810
+ *args: The positional arguments to pass to the function.
811
+ **kwargs: The keyword arguments to pass to the function.
812
+
813
+ Examples:
814
+ >>> rz.call(lambda x: x + 1, 41)
815
+ 42
816
+ >>> rz.call(rz.err('error'), 40, 2)
817
+ Err('error')
818
+
819
+ We can assign the error type using the indexing notation.
820
+
821
+ >>> rz.call[ZeroDivisionError](lambda x: 1 / x, 0)
822
+ Err(ZeroDivisionError('division by zero'))
823
+ """
824
+
825
+ @overload
826
+ def __getitem__[E: Exception, R, **P](
827
+ self, type: type[E]
828
+ ) -> Caller[P, Result[R, E]]: ...
829
+
830
+ @overload
831
+ def __getitem__[E1: Exception, E2: Exception, R, **P](
832
+ self, type: tuple[type[E1], type[E2]]
833
+ ) -> Caller[P, Result[R, E1 | E2]]: ...
834
+
835
+ @overload
836
+ def __getitem__[E1: Exception, E2: Exception, E3: Exception, R, **P](
837
+ self, type: tuple[type[E1], type[E2], type[E3]]
838
+ ) -> Caller[P, Result[R, E1 | E2 | E3]]: ...
839
+
840
+ @overload
841
+ def __getitem__[E1: Exception, E2: Exception, E3: Exception, E4: Exception, R, **P](
842
+ self, type: tuple[type[E1], type[E2], type[E3], type[E4]]
843
+ ) -> Caller[P, Result[R, E1 | E2 | E3 | E4]]: ...
844
+
845
+ @overload
846
+ def __getitem__[E: Exception, R, **P](
847
+ self, type: tuple[E, ...]
848
+ ) -> Caller[P, Result[R, E]]: ...
849
+
850
+ def __getitem__(self, type: Any) -> Any:
851
+ def call(*args: Any, **kwargs: Any) -> Any:
852
+ return call_checked(type, *args, **kwargs)
853
+
854
+ try:
855
+ call.__name__ = f"call[{type.__name__}]"
856
+ except AttributeError: # type is given as a tuple
857
+ call.__name__ = "call[...]"
858
+
859
+ return call
860
+
861
+ def __call__[**P, R](
862
+ self, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
863
+ ) -> Result[R, BaseException]:
864
+ if isinstance(fn, Err):
865
+ return fn
866
+ try:
867
+ return fn(*args, **kwargs)
868
+ except BaseException as e:
869
+ return Err(e)
870
+
871
+
872
+ call = _CallFactory()
873
+
874
+
875
+ def call_checked[**P, R, E: Exception](
876
+ errors: type[E] | tuple[type[E], ...],
877
+ fn: Callable[P, R],
878
+ /,
879
+ *args: P.args,
880
+ **kwargs: P.kwargs,
881
+ ) -> Result[R, E]:
882
+ """
883
+ Call the function with the given arguments and catch any exception of the given type.
884
+
885
+ Args:
886
+ exception: The type of exception to catch and return as an error.
887
+ fn: The function to call.
888
+ *args: The positional arguments to pass to the function.
889
+ **kwargs: The keyword arguments to pass to the function.
890
+
891
+ Examples:
892
+ >>> rz.call_checked(ZeroDivisionError, lambda x: 1 / x, 0)
893
+ Err(ZeroDivisionError('division by zero'))
894
+ >>> rz.call_checked(ValueError, lambda x: 1 / x, 0)
895
+ Traceback (most recent call last):
896
+ ...
897
+ ZeroDivisionError: division by zero
898
+ """
899
+ try:
900
+ return fn(*args, **kwargs)
901
+ except BaseException as e:
902
+ if isinstance(e, errors):
903
+ return Err(e)
904
+ raise e
905
+
906
+
907
+ @overload
908
+ def safe[**P, R]() -> Callable[[Callable[P, R]], Callable[P, Result[R, Exception]]]: ...
909
+
910
+
911
+ @overload
912
+ def safe[E: Exception, **P, R](
913
+ exception: type[E], /
914
+ ) -> Callable[[Callable[P, R]], Callable[P, Result[R, E]]]: ...
915
+
916
+
917
+ @overload
918
+ def safe[E1: Exception, E2: Exception, R, **P](
919
+ exception: tuple[type[E1], type[E2]], /
920
+ ) -> Callable[[Callable[P, R]], Callable[P, Result[R, E1 | E2]]]: ...
921
+
922
+
923
+ @overload
924
+ def safe[E1: Exception, E2: Exception, E3: Exception, R, **P](
925
+ exception: tuple[type[E1], type[E2], type[E3]], /
926
+ ) -> Callable[[Callable[P, R]], Callable[P, Result[R, E1 | E2 | E3]]]: ...
927
+
928
+
929
+ @overload
930
+ def safe[E1: Exception, E2: Exception, E3: Exception, E4: Exception, R, **P](
931
+ exception: tuple[type[E1], type[E2], type[E3], type[E4]], /
932
+ ) -> Callable[[Callable[P, R]], Callable[P, Result[R, E1 | E2 | E3 | E4]]]: ...
933
+
934
+
935
+ @overload
936
+ def safe[E: Exception, R, **P](
937
+ exception: Iterable[type[E]], /
938
+ ) -> Callable[[Callable[P, R]], Callable[P, Result[R, E]]]: ...
939
+
940
+
941
+ @overload
942
+ def safe(*args: Any) -> Any: ...
943
+
944
+
945
+ def safe(*errors: Any) -> Any:
946
+ if not errors:
947
+ return lambda fn: _wrap_safe(
948
+ fn, lambda *args, **kwargs: call(fn, *args, **kwargs)
949
+ )
950
+ else:
951
+ return lambda fn: _wrap_safe(
952
+ fn, lambda *args, **kwargs: call_checked(errors, fn, *args, **kwargs)
953
+ )
954
+
955
+
956
+ def getattr[T, R: type, E](
957
+ obj: Result[T, E], attr: str, *, type: type[R] = _any(None)
958
+ ) -> Result[R, E | AttributeError]:
959
+ """
960
+ Get the attribute of an object if it is not None, otherwise return None.
961
+
962
+ It raises an AttributeError if the attribute do not exist.
963
+
964
+ You can optionally specify the expected result type to get better type checking.
965
+
966
+ Args:
967
+ obj: The optional object to get the attribute from.
968
+ attr: The name of the attribute to get.
969
+ type: The expected type of the attribute (optional).
970
+
971
+ Examples:
972
+ >>> rz.getattr(42, "real")
973
+ 42
974
+ >>> rz.getattr(rz.err('error'), "imag")
975
+ Err('error')
976
+ >>> rz.getattr(42, "non_existent")
977
+ Err(AttributeError('non_existent'))
978
+ """
979
+ if isinstance(obj, Err):
980
+ return Err(obj.error)
981
+
982
+ try:
983
+ value = cast("R", _getattr(obj, attr))
984
+ except AttributeError:
985
+ return Err(AttributeError(attr))
986
+ if type is not None and not isinstance(value, type):
987
+ msg = (
988
+ f"Expected attribute {attr} to be of type {type}, "
989
+ f"got {builtins.type(value)}"
990
+ )
991
+ raise TypeError(msg)
992
+ return value
993
+
994
+
995
+ def _wrap_safe[F: Callable[..., Any]](fn: Any, impl: F) -> F:
996
+ try:
997
+ functools.update_wrapper(impl, fn)
998
+ except Exception:
999
+ return impl
1000
+ ret_type = impl.__annotations__.get("return", None)
1001
+ if ret_type is not None:
1002
+ impl.__annotations__["return"] = Result[ret_type, Exception] # type: ignore
1003
+ return impl
errorz/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ import doctest
2
+
3
+ import errorz
4
+
5
+ if __name__ == "__main__":
6
+ doctest.testmod(errorz, globs={"rz": errorz})
errorz/hypothesis.py ADDED
@@ -0,0 +1,76 @@
1
+ from string import ascii_letters, digits
2
+ from typing import Any, Iterable, cast
3
+
4
+ from hypothesis import strategies as st
5
+ from hypothesis.strategies import DrawFn
6
+
7
+ import errorz as rz
8
+
9
+ EXCEPTION_LIST = [Exception, ValueError, TypeError, KeyError, IndexError]
10
+
11
+
12
+ @st.composite
13
+ def result[T, E = Any](
14
+ draw: DrawFn, value: st.SearchStrategy[T], error: st.SearchStrategy[E] | None = None
15
+ ) -> rz.Result[T, E]:
16
+ """
17
+ Generate Results with random values.
18
+
19
+ Args:
20
+ value: A strategy to generate the ok values.
21
+ error: A strategy to generate the err values.
22
+ If None is given, it will generate random exceptions or primitive values as errors.
23
+ """
24
+
25
+ is_ok = draw(st.booleans())
26
+ if is_ok:
27
+ return rz.ok(draw(value))
28
+ elif error:
29
+ return rz.err(draw(error))
30
+ else:
31
+ return cast(
32
+ "rz.Err[E]",
33
+ rz.err(
34
+ draw(
35
+ st.one_of(
36
+ error_messages(),
37
+ exceptions(),
38
+ st.integers(),
39
+ )
40
+ )
41
+ ),
42
+ )
43
+
44
+
45
+ def exceptions(
46
+ exceptions: Iterable[type[BaseException]] | None = None,
47
+ ) -> st.SearchStrategy[BaseException]:
48
+ """
49
+ Generate exceptions with random messages.
50
+
51
+ By default, it generates a random exception from builtin python exceptions
52
+ with a random message.
53
+
54
+ You can also provide a custom list of exception classes to choose from.
55
+ They must accept a single string argument in their constructor.
56
+ """
57
+ if exceptions is None:
58
+ exceptions = EXCEPTION_LIST
59
+
60
+ return st.one_of([st.builds(exc, error_messages()) for exc in exceptions])
61
+
62
+
63
+ def error_messages() -> st.SearchStrategy[str]:
64
+ """
65
+ Generate random error messages.
66
+
67
+ Those are small strings with common letters. We are not trying to stress
68
+ test the string handling capabilities of our error types.
69
+ """
70
+ return st.one_of(
71
+ st.sampled_from(["error", "fail", "invalid", "..."]),
72
+ st.text(
73
+ alphabet=ascii_letters + digits + " ",
74
+ max_size=5,
75
+ ),
76
+ )
errorz/mypy.py ADDED
@@ -0,0 +1,53 @@
1
+ from functools import partial
2
+ from typing import Callable
3
+
4
+ import rich
5
+ from mypy.plugin import AnalyzeTypeContext, FunctionContext, Plugin
6
+ from mypy.types import Type
7
+
8
+ type TypeAnaliser = Callable[[AnalyzeTypeContext], Type]
9
+ type FunctionAnalyser = Callable[[FunctionContext], Type]
10
+
11
+
12
+ class ResultPlugin(Plugin):
13
+ def get_type_analyze_hook(self, fullname: str) -> TypeAnaliser | None:
14
+ # if fullname == "errorz.Result":
15
+ # return analyse_result_type
16
+ return None
17
+
18
+ def get_function_hook(self, fullname: str) -> FunctionAnalyser | None:
19
+ if fullname.startswith("builtins."):
20
+ return None
21
+
22
+ return partial(analyse_result_module_function, fullname)
23
+
24
+
25
+ def analyse_result_type(ctx: AnalyzeTypeContext) -> Type:
26
+ if ctx.context.line not in lines:
27
+ print(f"Analyzing type: {ctx.type} {ctx.context.line}")
28
+ lines.add(ctx.context.line)
29
+
30
+ try:
31
+ return ctx.api.analyze_type(ctx.type)
32
+ except Exception:
33
+ # print(f"Error analyzing type: {e}")
34
+ return ctx.type
35
+
36
+
37
+ def analyse_result_module_function(name: str, ctx: FunctionContext) -> Type:
38
+ if ctx.context.line < 840:
39
+ return ctx.default_return_type
40
+
41
+ print(f"Analyzing function: {name} (line {ctx.context.line})")
42
+ rich.print(ctx)
43
+
44
+ # print(vars(ctx))
45
+ return ctx.default_return_type
46
+
47
+
48
+ def plugin(version: str) -> type[ResultPlugin]:
49
+ print(f"Loading plugin for mypy version {version}")
50
+ return ResultPlugin
51
+
52
+
53
+ lines: set[int] = {842, 843, 844}
errorz/py.typed ADDED
File without changes
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: errorz
3
+ Version: 0.1.0
4
+ Summary: Represent fallible computations as values, instead of using exceptions.
5
+ Author: Fábio Macêdo Mendes
6
+ Author-email: Fábio Macêdo Mendes <fabiomacedomendes@gmail.com>
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Utilities
15
+ Maintainer: Fábio Macêdo Mendes
16
+ Maintainer-email: Fábio Macêdo Mendes <fabiomacedomendes@gmail.com>
17
+ Requires-Python: >=3.13
18
+ Project-URL: Homepage, http://github.com/fabiommendes/errorz
19
+ Project-URL: Repository, http://github.com/fabiommendes/errorz
20
+ Project-URL: Documentation, https://errorz.readthedocs.io/
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Errorz
24
+
25
+ Python officially handle errors using exceptions. This library provides a
26
+ lightweight approach inspired by the `Result` type used in Rust and other
27
+ functional programming languages: `Result` is an union between a success value
28
+ and an error value.
29
+
30
+ The main advantage over exceptions is that they are expressed as values in the
31
+ type system. This allows for better static guarantees and make code more
32
+ explicit and easier to compose, since results are just like any other value that
33
+ can be passed around and manipulated without cluncky try/catch blocks.
34
+ Exceptions also break control flow and can be easily forgotten and lead to
35
+ crashes and brittle refactorings.
36
+
37
+ The cannonical way to implement this pattern is to use a tagged union of `Ok[T]`
38
+ and `Err[E]`. This is what Rust and Haskell do and there are some Python
39
+ libraries that follow this approach, e.g.,
40
+ [returns](https://returns.readthedocs.io/). However, Python don't have native
41
+ tagged unions and using them often feels unergnomic and non-Pythonic. This
42
+ library explores a more lightweight approach that defines `Result[T, E] = T |
43
+ Error[E]`, where `Error[E]` is a special abstract wrapper around an error value.
44
+ This is simular to the approach we used in the
45
+ [optionz](https://optionz.readthedocs.io/) library for nullables, where it was
46
+ more lightweight and in line with established Python idioms.
47
+
48
+ ## Installation
49
+
50
+ Install Errorz using pip/uv/poetry whatever you like. For example:
51
+
52
+ ```bash
53
+ pip install errorz
54
+ ```
55
+
56
+ Errorz consists of a single file, so you can also just copy the `__init__.py`
57
+ file to your project as `errorz.py` and import it from there. Our special Error
58
+ wrapper is defined as a protocol and has some special structure to ensure that
59
+ different copies of the vendorized lib can coexist without conflicts.
60
+
61
+
62
+ ## Usage
63
+
64
+ Import `err` and use the functions as needed.
65
+
66
+ ```python
67
+ import errorz as rz
68
+
69
+ rz.unwrap(42) # 42
70
+ rz.unwrap(rz.err("error")) # raises ValueError
71
+ ```
72
+
73
+ ## Documentation
74
+
75
+ The documentation is available at https://optionz.rtfd.io/ and includes more
76
+ examples and explanations of the functions provided by the library.
77
+
78
+ ## License
79
+
80
+ Optionz is licensed under the MIT License. See [LICENSE](LICENSE) for more details.
@@ -0,0 +1,8 @@
1
+ errorz/__init__.py,sha256=ahht4Ka5Y5r-C2T7KEPoMefaXbNcoS_lAdt0IzS2ipw,24548
2
+ errorz/__main__.py,sha256=j1z1GICc-gIr49P0hUdd6KkfH2gENhtrhuJ21BtqlwI,108
3
+ errorz/hypothesis.py,sha256=3FSD5koHnE98LNwjpOjnFWcmDlM500S9Meisaum0Yx0,2135
4
+ errorz/mypy.py,sha256=iAuqPkxk879ZurINPLjeUmDZ-XioUTnmFDwymAEL8zc,1511
5
+ errorz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ errorz-0.1.0.dist-info/WHEEL,sha256=8ZlpUMJ7mlDirmlHRhDirEx_nPnARrwDjeE92mlk68E,81
7
+ errorz-0.1.0.dist-info/METADATA,sha256=TRbUywrclPUB_1CZIUgZ_o9QJV9GJAfAUIVqhjAPU7w,3148
8
+ errorz-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.21
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any