Error handling in configpile
Contents
Error handling in configpile
¶
The goal behind configpile
error reporting is to provide helpful error messages to the user. In
particular:
configpile
does not rely on Python exceptions, rather implements its own error classThe error class is designed to be used in a result type that follows existing Python usage patterns. (To be pedantic, it is not monadic.)
configpile
accumulate errors instead of stopping at the first errorInstead of relying on stack traces to convey contextual information,
configpile
errors store context information that is manually added when results are processed.
Errors¶
The base error type is Err
, which contains either a single error or a sequence of errors.
A single error is constructed through the make()
static method.
Errors can be pretty-printed. If the Rich library is available, some light formatting will be applied.
from configpile import Err
e1 = Err.make("First error", context_info = 1, other_info = "bla")
e1.pretty_print()
In other_info: bla
In context_info: 1
First error
Errors can be collected in a single Err
instance, and pretty-printing will collect errors occurring in the same context.
e1 = Err.make("First error", context_info = 1, other_info = "blub")
e2 = Err.make("Second error", context_info = 1)
e12 = Err.collect1(e1, e2)
e12.pretty_print()
• In context_info: 1 0 In other_info: blub First error 1 Second error
A sequence of single errors can always be recovered:
e12.errors()
[Err1(msg='First error', contexts=[('context_info', 1), ('other_info', 'blub')]),
Err1(msg='Second error', contexts=[('context_info', 1)])]
Results¶
The error type is designed to be used in functions that either return a valid value, or an error.
Such functions return a result, or a configpile.userr.Res
type.
Note that the configpile.userr.Res
type is parameterized by the valid value type:
in the example below, it is int
.
An example of such a function would be:
from configpile.userr import Res
def parse_int(s: str) -> Res[int]:
try:
return int(s)
except ValueError as e:
return Err.make(str(e))
and would give the following results:
parse_int("invalid")
Err1(msg="invalid literal for int() with base 10: 'invalid'", contexts=[])
parse_int(1234)
1234
Results can be processed further. For example, the function that squares the value contained in a result, while leaving any error untouched, can be written:
def square_result(res: Res[int]) -> Res[int]:
if isinstance(res, Err):
return res
return res*res
… or, using the map()
helper:
from configpile import userr
def square_result1(res: Res[int]) -> Res[int]:
return userr.map(lambda x: x*x, res)
and we have, unsurprisingly:
square_result(parse_int("invalid"))
Err1(msg="invalid literal for int() with base 10: 'invalid'", contexts=[])
square_result1(parse_int(4))
16
The flat_map()
function is useful to chain processing where each step can fail.
import math
def square_root(x: int) -> Res[float]:
if x < 0:
return Err.make(f"Cannot take square root of negative number {x}")
else:
return math.sqrt(float(x))
userr.flat_map(square_root, parse_int("valid"))
Err1(msg="invalid literal for int() with base 10: 'valid'", contexts=[])
userr.flat_map(square_root, parse_int("2"))
1.4142135623730951
userr.flat_map(square_root, parse_int("-2"))
Err1(msg='Cannot take square root of negative number -2', contexts=[])
Combining results and errors¶
Finally, the userr
module offers ways to combine results.
For example, if one parses several integers, one can collect the results in a tuple using the
configpile.userr.collect()
function.
userr.collect(parse_int(2), parse_int(3))
(2, 3)
userr.collect(parse_int(3), parse_int("invalid"))
Err1(msg="invalid literal for int() with base 10: 'invalid'", contexts=[])
userr.collect(parse_int("invalid"), parse_int("invalid"))
ManyErr(errs=[Err1(msg="invalid literal for int() with base 10: 'invalid'", contexts=[]), Err1(msg="invalid literal for int() with base 10: 'invalid'", contexts=[])])
See also configpile.userr.collect_seq()
when dealing with sequences.
Errors can be collected and combined too. The configpile.userr.Err.collect1()
method expect
at least one argument and returns an Err
, while
configpile.userr.Err.collect()
can deal with no argument being passed, or with optional
arguments.
In particular, optional errors, of type Optional[Err]
, are great for validation: a None
value indicates no error, while an error indicates that one or several problems are present.
from typing import Optional, Sequence
a = -2
b = 1
check_a: Optional[Err] = Err.check(a > 0, "a must be positive")
check_b: Optional[Err] = Err.check(b > 0, "b must be positive")
Err.collect(check_a, check_b)
Err1(msg='a must be positive', contexts=[])