Skip to content

Commit f0fb3d6

Browse files
committed
v0.3.0
1 parent e2447f6 commit f0fb3d6

File tree

3 files changed

+275
-139
lines changed

3 files changed

+275
-139
lines changed

README.md

Lines changed: 186 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
<p align="left">
44
<img src="https://github.com/ScottBoyce-Python/ResultContainer/actions/workflows/ResultContainer-pytest.yml/badge.svg" alt="Build Status" height="20">
55
</p>
6+
## About
67

8+
ResultContainer is a Python library inspired by [Rust’s Result](https://doc.rust-lang.org/std/result/enum.Result.html) enum, designed for robust error handling. It seamlessly supports mathematical operations, attribute access, and method chaining on `Ok(value)`, while automatically transitioning to `Err(e)` upon encountering errors, ensuring continuous error tracking and logging. Ideal for developers seeking structured and functional error management in Python.
79

10+
## Description
811

9-
The `ResultContainer` module simplifies complex error handling into clean, readable, and maintainable code structures. Error handling in Python can often become unwieldy, with deeply nested `try/except` blocks and scattered error management. The `ResultContainer` is used for situations when errors are expected and are easily handled. Inspired by [Rusts Result<Ok, Err>](https://doc.rust-lang.org/std/result/enum.Result.html) enum, `ResultContainer` introduces a clean and Pythonic way to encapsulate success (`Ok`) and failure (`Err`) outcomes.
12+
The `ResultContainer` module simplifies complex error handling into clean, readable, and maintainable code structures. Error handling in Python can often become unwieldy, with deeply nested `try/except` blocks and scattered error management. The `ResultContainer` is used for situations when errors are expected and are easily handled. Inspired by Rust's Result<Ok, Err> enum, `ResultContainer` introduces a clean and Pythonic way to encapsulate a `Result` as a success (`Ok`) and failure (`Err`) outcomes.
1013

11-
The `ResultContainer.Result` enum wraps a value in an `Ok` variant, until there is an exception or error raised, and then it is converted to the `Err` variant. The `Err` variant wraps a `ResultContainer.ResultErr` exception object that contains the error messages and traceback information. The `Result` object includes similar methods to the Rust Result Enum for inquiry about the state, mapping functions, and passing attributes/methods to the containing `value`.
14+
The `ResultContainer` module contains two classes, `ResultErr` and `Result`. The `ResultContainer.ResultErr` class extends the Exception class to collect and store error messages and traceback information. The `ResultContainer.Result` is the enum with two variants: `ResultContainer.Result.Ok(value)` and `ResultContainer.Result.Err(e)`. The `Ok(value)` variant wraps any `value` as long as no exceptions occur. The `Ok` variant cannot directly wrap another Result or ResultErr objects. However, `Ok` can wrap another object, such as a list, that contains Result and ResultErr objects. Methods and attributes that are not part of the Result class are automatically passed to the wrapped `value`. For example, `Ok(value).method()` becomes `Ok(value.method())`. If an `Ok` variant operation results in raising an exception, the exception and traceback info is stored in a `ResultErr` object (`e`) and the `Ok` is converted to the `Err(e)` variant. Subsequent errors are appended to `e`. `Result` contains status inquiry methods, unwrap methods to get the stored `value` or `e`, and the ability to raise a `ResultErr` exception for the `Err(e)` variant.
1215

1316
The `ResultContainer` is designed to streamline error propagation and improve code readability, `ResultContainer` is ideal for developers seeking a robust, maintainable approach to handling errors in data pipelines, API integrations, or asynchronous operations.
1417

@@ -31,31 +34,39 @@ git clone https://github.com/ScottBoyce-Python/ResultContainer.git
3134
```
3235
then rename the file `ResultContainer/__init__.py` to `ResultContainer/ResultContainer.py` and move `ResultContainer.py` to wherever you want to use it.
3336

37+
38+
3439
## Variants
3540

3641
```python
3742
# Result is the main class and Ok and Err are constructors.
38-
from ResultContainer import Result, Ok, Err
43+
from ResultContainer import Result, Ok, Err, ResultErr
3944
```
4045

4146
- `Ok(value)`
42-
- `value` is wrapped within an `Ok`.
47+
- `value` is any object to be wrapped within an `Ok`.
4348
- Constructor: `Result.as_Ok(value)`
4449
- `Result.Ok` attribute returns the wrapped `value`
4550
- Can never wrap a `ResultErr` instance (it will just be converted to an `Err(value)`).
4651

4752
- `Err(e)`
48-
- `e` is wrapped within an `Err`, and `type(e) is ResultErr`.
53+
- `e` is any object to be wrapped within an `Err`.
54+
- `e` is stored as `ResultErr` Exception object.
55+
- If `not isinstance(e, ResultErr)`, then `e = ResultErr(e)`.
4956
- Constructor: `Result.as_Err(error_msg)`
5057
- `Result.Err` attribute returns the wrapped `e`.
5158

5259
### Properties of the `Result` Variants
5360

5461
#### `Err(e)`:
5562

56-
- Represents a failure (error-state) and contains `e` as a `ResultErr` object that stores error messages and traceback information.
63+
- Represents a failure (error-state).
64+
- `e` is a `ResultErr` object that stores error messages and traceback information.
5765

58-
- Can be initialized with `Err(error_msg)`
66+
- If `e` is another type, it is converted to a `ResultErr`.
67+
That is, given `Err(e)` and `not isinstance(e, ResultErr)` becomes `Err( ResultErr(e) )`.
68+
69+
- Can be initialized with `Err(error_msg)`, where `error_msg` can be any object (typically a str)
5970
- `Err(e)` &nbsp;&nbsp; syntactic-sugar for &nbsp;&nbsp; `Result.as_Err(e)`
6071

6172
- If an `Ok(value)` operation fails, then it is converted to an `Err(e)`, where `e` stores the error message.
@@ -94,14 +105,79 @@ from ResultContainer import Result, Ok, Err
94105
- `Err(e1) < Err(e2) ``False`
95106
- `Err(e1) <= Err(e2) ``True`
96107

97-
## Initialization
108+
109+
110+
## ResultErr Class
111+
112+
The `ResultErr` class is a custom exception class for error handling in the `Result` object. The `ResultErr` class captures error messages and optional traceback information. Its primary use is for identifying when a `Result` instance is an `Err` variant, which is handled automatically. It should not be necessary to use the ResultErr class directly, but select attributes and methods are presented here for background information.
113+
114+
### Initialization
115+
116+
```python
117+
# All arguments are optional
118+
from ResultContainer import ResultErr
119+
120+
# Main object signature:
121+
e = ResultErr(msg="", add_traceback=True, max_messages=20)
122+
# msg (Any, optional): Error message(s) to initialize with.
123+
# `str(msg)` is the message that is stored.
124+
# If msg is a Sequence, then each item in the Sequence is
125+
# appended as str(item) to the error messages.
126+
# Default is "", to disable error status.
127+
# add_traceback (bool, optional): If True, then traceback information is added to the message.
128+
# max_messages (int, optional): The maximum number of error messages to store.
129+
# After this, all additional messages are ignored. Default is 20.
130+
```
131+
132+
### Attributes and Methods
133+
134+
These are select attributes and methods built into `Result` object.
135+
136+
#### Attributes
137+
138+
```
139+
size (int): Returns the number of error messages.
140+
141+
is_Ok (bool): Returns False if in error status (ie, size == 0).
142+
is_Err (bool): Returns True if in error status (ie, size > 0).
143+
144+
Err_msg (list[str]): List of the error messages that have been added.
145+
Err_traceback (list[list[str]]): List of lists that contains the traceback information for each error message.
146+
```
147+
148+
#### Methods
149+
150+
```
151+
append(msg, add_traceback=True):
152+
Append an error message to the instance.
153+
154+
raises(add_traceback=False, error_msg=""):
155+
Raise a ResultErr exception if `size > 0`.
156+
`error_msg` is an optional note to append to the ResultErr.
157+
If not exception is raised, then returns itself.
158+
159+
str(sep=" | ", as_repr=True, add_traceback=False):
160+
Returns a string representation of the error messages and traceback information.
161+
If as_repr is True error messages are be printed inline (repr version),
162+
while False writes out traceback and error messages over multiple lines (str version).
163+
For general use, it is recomended to use the default values.
164+
165+
copy():
166+
Return a copy of the current ResultErr object.
167+
```
168+
169+
170+
171+
## Result Class
172+
173+
### Initialization
98174

99175
```python
100176
# Only the first argument is required for all constructors
101177
from ResultContainer import Result, Ok, Err
102178

103179
# Main object signature:
104-
res = Result(value, success, error_msg, add_traceback, deepcopy) # Construct either Ok or Er
180+
res = Result(value, success, error_msg, add_traceback, deepcopy) # Construct either Ok or Err
105181

106182
# Classmethod signatures:
107183
res = Result.as_Ok(value, deepcopy) # Construct Ok variant
@@ -129,11 +205,11 @@ res = Err(error_msg, add_traceback) # Construct Er
129205
#
130206
```
131207

132-
## Attributes and Methods
208+
### Attributes and Methods
133209

134210
These are select attributes and methods built into `Result` object. For a full listing please see the Result docstr.
135211

136-
### Attributes
212+
#### Attributes
137213

138214
```
139215
is_Ok (bool): True if the result is a success.
@@ -156,7 +232,7 @@ Err_traceback (list[list[str]]):
156232
For the Err(e) variant, returns list of traceback lines.
157233
```
158234

159-
### Methods
235+
#### Methods
160236

161237
```
162238
raises(add_traceback=False, error_msg=""):
@@ -201,7 +277,7 @@ apply_map(ok_func, unwrap=False):
201277
202278
map(ok_func):
203279
map_or(default, ok_func):
204-
Same functionality as apply and apply_or,
280+
Same functionality as apply and apply_or,
205281
except that if the function call fails, raises an exception.
206282
207283
iter(unwrap=True, expand=False):
@@ -239,14 +315,40 @@ copy(deepcopy=False):
239315
240316
```
241317

242-
243-
318+
244319

245320
## Usage
246321

247322
Below are examples showcasing how to create and interact with a `ResultContainer`.
248323

249-
### Creating a Result
324+
### ResultErr Initialization
325+
326+
```python
327+
from ResultContainer import ResultErr
328+
329+
# Initialize ResultErr instances
330+
a = ResultErr("Error Message")
331+
b = ResultErr(5)
332+
c = ResultErr(["Error Message 1", "Error Message 2"])
333+
334+
print(a.str()) # ResultErr("Error Message")
335+
print(b.str()) # ResultErr("5")
336+
print(c.str()) # ResultErr("Error Message 1 | Error Message 2")
337+
338+
raise c # Raises the following exception:
339+
340+
# Traceback (most recent call last):
341+
# File "example.py", line 12, in <module>
342+
# raise c
343+
# ResultContainer.ResultErr:
344+
# File "example.py", line 6, in <module>
345+
# c = ResultErr(["Error Message 1", "Error Message 2"])
346+
#
347+
# <Error Message 1>
348+
# <Error Message 2>
349+
```
350+
351+
### Result Initialization
250352

251353
```python
252354
from ResultContainer import Result, Ok, Err, ResultErr
@@ -272,12 +374,14 @@ a = Err(5)
272374
# A ResultErr instance is always wrapped by Err ---------------------------------------------------------
273375
e = ResultErr("Error Message") # e is an instance of ResultErr
274376

275-
a1 = Result(e, success=True) # a1 == a2 == a3 == Err(e)
276-
a2 = Result.as_Ok(5)
377+
a1 = Result(e, success=True) # a1 == a2 == a3 == Err(e); success is ignored because isinstance(e, ResultErr)
378+
a2 = Result.as_Ok(e)
277379
a3 = Ok(e)
278380
```
279381

280-
### Math Operations with a Result
382+
383+
384+
### Math Operations
281385

282386
```python
283387
from ResultContainer import Ok
@@ -299,7 +403,9 @@ z = x + y # z = Ok([1, 2, 3, 4, 5, 6, 7])
299403

300404
```
301405

302-
### Wrapping Objects
406+
407+
408+
### Wrapping `datetime Example
303409

304410
```python
305411
from ResultContainer import Result, Ok, Err
@@ -323,6 +429,32 @@ bad_dt = dt + timedelta(days=10000) # bad_dt = Err("a + b resulted in an
323429
bad_dt.raises() # raises a ResultErr exception
324430
```
325431

432+
433+
434+
### Passing Functions and Chaining Operations
435+
436+
```python
437+
from ResultContainer import Result, Ok, Err
438+
from math import sqrt
439+
# to use an external function, like sqrt
440+
# It must be passed to either apply or map or extracted with expect.
441+
# apply converts Ok to Err if the func fails, while map raises an exception.
442+
a = Ok(9) # Ok(9)
443+
b = a.apply(sqrt) # Ok(3.0)
444+
c = Ok(-9) # Ok(-9)
445+
d = c.apply(sqrt) # Err("Result.apply exception. | ValueError: math domain error")
446+
e = sqrt(c.expect()) # raises an error
447+
448+
plus1 = lambda x: x + 1
449+
a = Ok(5)
450+
b = Ok(0)
451+
c = (a / b).map_or(10, plus1).map_or(20, plus1).map_or(30, plus1) # c = Err() -> Ok(10) -> Ok(11) -> Ok(12)
452+
d = (a / 0).map_or(10, plus1).map_or(20, plus1).map_or(30, plus1) # d = Err() -> Ok(10) -> Ok(11) -> Ok(12)
453+
454+
```
455+
456+
457+
326458
### Raising Errors
327459

328460
```python
@@ -337,67 +469,51 @@ x /= 0 # x = Err("a /= b resulted in an Exception. | ZeroDivisionError
337469
y = x.raises() # Raises the following exception:
338470

339471
# Traceback (most recent call last):
472+
# File "example.py", line 9, in <module>
473+
# y = x.raises() # Raises the following exception:
474+
# ^^^^^^^^^^
475+
# File "ResultContainer\__init__.py", line 882, in raises
476+
# raise self._val # Result.Err variant raises an exception
477+
# ^^^^^^^^^^^^^^^
478+
# ResultContainer.ResultErr:
340479
# File "example.py", line 7, in <module>
341-
# x.raises()
342-
# ^^^^^^^^^^
343-
# File "ResultContainer/ResultContainer.py", line 957, in raises
344-
# raise self._Err
345-
# ResultErr:
346-
# [1] a /= b resulted in an Exception.
347-
# [12] ZeroDivisionError: division by zero
480+
# x /= 0 # x = Err("a /= b resulted in an Exception. | ZeroDivisionError: division by zero")
481+
# File "ResultContainer\__init__.py", line 1313, in __itruediv__
482+
# return self._operator_overload_error(e, op, True)
483+
#
484+
# <a /= b resulted in an Exception.>
485+
# <ZeroDivisionError: division by zero>
486+
# <Result.raises() on Err>
348487
```
349488

350489

351490

352-
353491
```python
354-
1 | from ResultContainer import Result, Ok, Err
355-
2 | from datetime import datetime, timedelta
356-
3 |
357-
4 | dt = Result(datetime(9999, 12, 31))
358-
5 |
359-
6 | bad_dt = dt + timedelta(days=10000)
360-
7 |
361-
8 | bad_dt.raises()
362-
# Raises the following exception.
363-
# Note the exception says it occured on `line 6` despite being called on `line 8`
492+
from ResultContainer import Result, Ok, Err
493+
from datetime import datetime, timedelta
364494

495+
dt = Result(datetime(9999, 12, 31))
496+
497+
bad_dt = dt + timedelta(days=10000)
498+
499+
bad_dt.raises() # Raises the following exception:
365500
# Traceback (most recent call last):
366501
# File "example.py", line 8, in <module>
367-
# bad_dt.raises()
368-
# File "ResultContainer/ResultContainer.py", line 957, in raises
369-
# raise self._Err
370-
# ResultErr:
371-
# File "ResultContainer/example.py", line 6, in <module>
502+
# bad_dt.raises()
503+
# ^^^^^^^^^^^^^^^
504+
# File "ResultContainer\__init__.py", line 882, in raises
505+
# raise self._val # Result.Err variant raises an exception
506+
# ^^^^^^^^^^^^^^^
507+
# ResultContainer.ResultErr:
508+
# File "example.py", line 6, in <module>
372509
# bad_dt = dt + timedelta(days=10000)
373-
#
374-
# [1] a + b resulted in an Exception.
375-
# [12] OverflowError: date value out of range
510+
#
511+
# <a + b resulted in an Exception.>
512+
# <OverflowError: date value out of range>
513+
# <Result.raises() on Err>
376514
```
377515

378-
### Passing Functions and Chaining Operations
379-
380-
```python
381-
from ResultContainer import Result, Ok, Err
382-
from math import sqrt
383-
# to use an external function, like sqrt
384-
# It must be passed to either apply or map or extracted with expect.
385-
# apply converts Ok to Err if the func fails, while map raises an exception.
386-
a = Ok(9) # Ok(9)
387-
b = a.apply(sqrt) # Ok(3.0)
388-
c = Ok(-9) # Ok(-9)
389-
d = c.apply(sqrt) # Err("Result.apply exception. | ValueError: math domain error")
390-
e = sqrt(c.expect()) # raises an error
391-
392-
plus1 = lambda x: x + 1
393-
a = Ok(5)
394-
b = Ok(0)
395-
c = (a / b).map_or(10, plus1).map_or(20, plus1).map_or(30, plus1) # c = Err() -> Ok(10) -> Ok(11) -> Ok(12)
396-
d = (a / 0).map_or(10, plus1).map_or(20, plus1).map_or(30, plus1) # d = Err() -> Ok(10) -> Ok(11) -> Ok(12)
397-
398-
```
399-
400-
516+
401517

402518
## Testing
403519

@@ -413,7 +529,7 @@ pytest # run all tests, note options are set in the pyproject.toml file
413529

414530
Note, that the [pyproject.toml](pyproject.toml) contains the flags used for pytest.
415531

416-
532+
417533

418534
## License
419535

0 commit comments

Comments
 (0)