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 +1003 -0
- errorz/__main__.py +6 -0
- errorz/hypothesis.py +76 -0
- errorz/mypy.py +53 -0
- errorz/py.typed +0 -0
- errorz-0.1.0.dist-info/METADATA +80 -0
- errorz-0.1.0.dist-info/RECORD +8 -0
- errorz-0.1.0.dist-info/WHEEL +4 -0
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
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,,
|