Skip to content

Commit 3eba6d7

Browse files
author
Sylvain MARIE
committed
Replaced the old documentation about what is accepted in terms of validation functions. Also explaining how to convert an existing function to a failure raiser.
1 parent 1f36744 commit 3eba6d7

2 files changed

Lines changed: 165 additions & 28 deletions

File tree

docs/decorators/a_requirements.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Requirements
2+
3+
## Accepted validation functions
4+
5+
There seem to be two main styles out there when it comes to writing validation functions:
6+
7+
* *boolean testers* such as `isfinite` : they return `True` in case of success and `False` in case of failure, and therefore do not provide any details about the failure when they fail. Sometimes they continue to raise exceptions on edge cases (`isfinite(None)`, `isfinite(1+1j)`).
8+
9+
* *failure raisers* such as `check_uniform_sampling` or `assert_series_equal`: they do not return anything in case of success, but raise exceptions with details in case of failure.
10+
11+
In order to be as open as possible, the definition of accepted functions in `valid8` is very large. Is considered a 'valid' validation function any callable that:
12+
13+
* takes a single argument as input
14+
* returns `True` or `None` in case of success
15+
16+
That's the two only requirements. That means that base validation functions **may fail the way they like**: returning `False` or something else, raising `Exception`s.
17+
18+
### Name used in error messages
19+
20+
In validation error messages, the name of the function that will be displayed is obtained from the validation callable `v_callable` with the following formula:
21+
22+
```python
23+
name = v_callable.__name__ if hasattr(v_callable, '__name__') else str(v_callable)
24+
```
25+
26+
## Creating *failure raisers* for better user experience
27+
28+
As explained above, nothing else than returning `True` or `None` in case of success is required by `valid8`. However when creating your own base functions you might wish to create *failure raisers* rather than *boolean testers* because in case of failure they can provide **many useful details in the raised exception**. This is how you can do it:
29+
30+
### 1. Writing your own
31+
32+
You may wish to create a custom validation function that directly raises an instance or subclass of the `valid8.ValidationFailure` class: it provides a simple way to define help messages as class members, with a templating mechanism. All functions in the built-in library are done that way.
33+
34+
For example this is the code for the `non_empty` validation function:
35+
36+
```python
37+
from valid8 import ValidationFailure
38+
39+
class Empty(ValidationFailure, ValueError):
40+
""" Custom ValidationFailure raised by non_empty """
41+
help_msg = 'len(x) > 0 does not hold for x={wrong_value}'
42+
43+
44+
def non_empty(x):
45+
"""
46+
'non empty' validation function. Raises a `Empty` error in case of failure.
47+
"""
48+
if len(x) > 0:
49+
return True
50+
else:
51+
raise Empty(wrong_value=x)
52+
```
53+
54+
You can find some inspiration [here](https://github.com/smarie/python-valid8/blob/master/valid8/validation_lib/collections.py).
55+
56+
Sometimes it might be easier for a quick test, to add the "failure raiser" facet to one of your existing functions. For this you can use the `@as_failure_raiser` decorator:
57+
58+
```python
59+
from valid8 import as_failure_raiser
60+
61+
@as_failure_raiser(help_msg='x should be strictly positive')
62+
def is_strictly_positive(x):
63+
return x > 0
64+
```
65+
66+
67+
### 2. Enriching an existing function
68+
69+
An alternative is to transform existing functions into failure raisers by adding help messages or custom `ValidationFailure` subtypes.
70+
71+
#### a - on the fly in the entry point
72+
73+
Most `valid8` entry points and composition operators support the *simple validation function definition syntax* explained [here](./b_simple_syntax.md). Thanks to this syntax, you can transform existing functions into failure raisers on the fly, when you use them.
74+
75+
For example you can add a custom message to `isfinite`:
76+
77+
```python
78+
>>> from valid8 import validate
79+
>>> from math import inf, isfinite
80+
>>> x = inf
81+
>>> validate('x', x, custom={'x is not finite': isfinite})
82+
83+
valid8.entry_points.ValidationError[ValueError]: \
84+
Error validating [x=inf]. InvalidValue: x is not finite. \
85+
Function [isfinite] returned [False] for value inf.
86+
```
87+
88+
You can also specify a custom failure class that should be raised:
89+
90+
```python
91+
>>> from valid8 import validate, ValidationFailure
92+
>>> from math import inf, isfinite
93+
>>> class NotFinite(ValidationFailure):
94+
... help_msg = "x is not finite"
95+
...
96+
>>> x = inf
97+
>>> validate('x', x, custom={NotFinite: isfinite})
98+
99+
valid8.entry_points.ValidationError[ValueError]: \
100+
Error validating [x=inf]. NotFinite: x is not finite. \
101+
Function [isfinite] returned [False] for value inf.
102+
```
103+
104+
#### b - permanently for reuse
105+
106+
If you wish to reuse a validation function in many places, it might be simpler to convert it to a failure raiser once.
107+
You can transform an existing validation function in a failure raiser with `failure_raiser()`:
108+
109+
```python
110+
from valid8 import failure_raiser
111+
from math import isfinite
112+
113+
# custom message only
114+
new_func = failure_raiser(isfinite, help_msg='x is not finite')
115+
116+
# custom failure type
117+
new_func = failure_raiser(isfinite, failure_type=NotFinite)
118+
```
119+
120+
You can do the same with the `@as_failure_raiser` decorator already presented above.
121+
122+
123+
### Docstring
124+
125+
#### failure_raiser
126+
127+
```python
128+
def failure_raiser(validation_callable, # type: ValidationCallableOrLambda
129+
help_msg=None, # type: str
130+
failure_type=None, # type: Type[ValidationFailed]
131+
**kw_context_args):
132+
# type: (...) -> ValidationCallable
133+
```
134+
135+
Wraps the provided validation function so that in case of failure it raises the given `failure_type` or a
136+
`WrappingFailure`ValidationFailed help message.
137+
138+
mini-lambda functions are automatically transformed to functions.
139+
140+
See `help(failure_raiser)`
141+
142+
#### @as_failure_raiser
143+
144+
```python
145+
def as_failure_raiser(failure_type=None, # type: Type[ValidationFailure]
146+
help_msg=None, # type: str
147+
**kw_context_args):
148+
```
149+
150+
A decorator to define a failure raiser. Same functionality then `failure_raiser`:
151+
152+
```python
153+
from valid8 import as_failure_raiser
154+
155+
@as_failure_raiser(help_msg="x should be smaller than 4")
156+
def is_small_with_details(x):
157+
return x < 4
158+
```
159+
160+
>>> is_small_with_details(2)
161+
>>> is_small_with_details(11)
162+
Traceback (most recent call last):
163+
...
164+
valid8.base.InvalidValue: x should be smaller than 4. Function [is_small_with_details] returned [False] for
165+
value 11.

docs/decorators/accepted.md

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)