valid8 (original) (raw)

python-validate (valid8)

Python versions Build Status Tests Status codecov

Documentation PyPI Downloads Downloads per week GitHub stars

"valid8ing is not a crime" ;-)

New vtypes and pyfields libraries leverage valid8 to provide higher-level value, check them out !

Major API changes in version 5.0.0. See changelog for details.

Overview

valid8 provides user-friendly tools for 3 kind of "entry points":

All these entry points raise consistent ValidationError including user-friendly details, with inheritance of ValueError / TypeError as appropriate. You can customize this error so as to get unique error types convenient for i18n.

Now what can you validate ? Pretty much anything: you define validators by combining one or several elementary validation functions, with minimalistic requirements. For this you can reuse built-in functions, 3d party functions, or write your own. You can even augment existing functions with more user-friendly errors. The internal composition framework makes it easy to combine the best of all worlds for each situation.

So with a bird eye's view:

architecture_overview

Do I need validation ?

type (TypeError) and value (ValueError) validation is key to smooth user experience: if something fails, there is nothing more frustrating that a horrible cryptic error message. Good validation leads to less end-user frustration. So validation has to be done in user-facing production code. However it is maybe already done by someone else:

In this page, we try to explain what is validation, what you would need to do in order to implement it correctly and consistently by yourself (assert is not a viable solution unfortunately), and demonstrate how much effort that would cost you.

I searched and found tons of good and less good libraries out there, but did not find any that would provide a somehow generic answer covering 80% of the needs - especially the particular needs I had for class attributes validation in autoclass, attrs and now in pyfields.

valid8 provides tools to perform validation in a minimum number of lines and with built-in consistency, so that it hopefully becomes fun and easy to include in your code.

Installing

Optional but recommended:

0. Concepts

valid8 relies on two main concepts:

1. Entry points

a - inline

valid8 provides two validation methods that you may use anywhere in your code:

validate

The validate method is the most straightforward tool in this toolbox. It provides you with ways to quickly validate one or several of:

For example:

`from valid8 import validate

surf = -1 validate('surface', surf, instance_of=int, min_value=0) `

results in

ValidationError[ValueError]: Error validating [surface=-1]. \ TooSmall: x >= 0 does not hold for x=-1. Wrong value: -1.

Note that the resulting exception object contains much of the available information (var_name, var_value, failure) as fields.

All functions from the built-in validation library are supported as explicit arguments as shown above, so that the method execution is the fastest possible. In addition you can add any other validation function with the custom argument:

`from mini_lambda import x

surf = 20 validate('surface', surf, instance_of=int, min_value=0, custom=[x ** 2 < 50]) `

yields

ValidationError[ValueError]: Error validating [surface=20]: \ validation function [x ** 2 < 50] returned [False].

Here the custom validation function is implemented using mini-lambda, but any function will do ; the only requirement is that your function accepts one positional input and returns True or None in case of success.

You can easily define custom validation functions with composition, custom failure messages or even custom failure types. For example these are accepted:

`# custom failure message validate('surface', surf, instance_of=int, min_value=0, custom=[(x ** 2 < 50, 'x should be fairly small')])

composition with detailed failure messages

validate('surface', surf, instance_of=int, custom={'x should be fairly small': x ** 2 < 50, 'x should be a multiple of 3': x % 3 == 0}) `

See API reference for usage details, and below validation functions and validation errors to explore the various customization possibilities.

validator/validation

The validator (alias validation) context manager allows you to define the validation procedure yourself, with any statement or function of your choice.

If you rely on functions that output a boolean flag indicating success ("boolean testers"), you simply have to plug that flag on the object provided by the context manager, as shown below:

`from valid8 import validator

surf = -1

with validator('surface', surf) as v: v.alid = surf > 0 `

Which yields

ValidationError[ValueError]: Error validating [surface=-1]: \ validation function [v.alid = surf > 0] returned [False].

Note that the text of the error displays the source code that led to that failure.

If you rely on functions that may raise exceptions ("failure raisers"), validator will catch them and append them as the inner failure of the resulting ValidationError:

`from math import isfinite

surf = 1j

with validator('surface', surf) as v: v.alid = surf > 0 and isfinite(surf) `

yields

ValidationError[TypeError]: Error validating [surface=1j]. \ InvalidType: Function [v.alid = surf > 0 and isfinite(surf)] raised \ TypeError: '>' not supported between instances of 'complex' and 'int'.

With validator you are therefore guaranteed that any exception happening in the inner code block will be caught and turned into a friendly ValidationError, whatever the cause (None handling, known validation failures, unknown other errors). You therefore do not have to write the usual if/else/try/except wrappers to handle all cases.

A couple important things to note:

with validation('dataframe', df, instance_of=pd.DataFrame): check_uniform_sampling(df) assert_series_equal(df['a'], ref_series)

**Note on type checking with isinstance**Although you can use the built-in isinstance method inside your validation code block, if you do it valid8 will not be able to distinguish between TypeError and ValueError. Instead, please consider either

with validator('surface', surf, instance_of=int) as v: v.alid = surf > 0

`from valid8 import assert_instance_of, assert_subclass_of

with validator('surface', surf) as v: assert_instance_of(surf, int) v.alid = surf > 0 `

See API reference for usage details, and below validation functions and custom exceptions to explore the various customization possibilities.

b - functions

The @validate_arg decorator adds input validation to a function. Here is an example where we decorate a build_house function:

`from mini_lambda import s, Len from valid8 import validate_arg from valid8.validation_lib import instance_of

@validate_arg('name', instance_of(str), Len(s) > 0, help_msg='name should be a non-empty string') def build_house(name, surface=None): print('Building house... DONE !') return name, surface `

As you can see above, any number of base validation functions can be provided to the decorator - this will correspond to an implicit and_ being performed, equivalent to one that you would have done with the built-in composition framework. Here we validate the input argument name by chaining the function instance_of(str) (from the built-in validation functions library) with a custom lambda function Len(s) > 0 created using the mini_lambda syntax.

Let's try it:

`> build_house('sweet home', 200) # Valid

build_house('', 200) InputValidationError[ValueError]: name should be a non-empty string.
Error validating input [name=''] for function [build_house].
At least one validation function failed for value ''.
Successes: ["instance_of_<class 'str'>"]
/ Failures: {'len(s) > 0': 'Returned False.'}. `

We can see in this verbose exception all details of interest: first the custom help_msg; then the validated function and input name; and finally the composition failure resulting from one of the two base functions failing (len(s) > 0 as stated). Note that since Len is minilambda's equivalent of len, the string representation is lowercase

The @validate_out function decorator works the same way for output validation, except that you don't provide it with any name.

nonable inputs/outputs

Here is a more complete example with both input and output validation:

`from mini_lambda import s, x, l, Len from valid8 import validate_arg, validate_out from valid8.validation_lib import instance_of, is_multiple_of

@validate_arg('name', instance_of(str), Len(s) > 0, help_msg='name should be a non-empty string') @validate_arg('surface', (x >= 0) & (x < 10000), is_multiple_of(100), help_msg='Surface should be a multiple of 100 between 0 and 10000.') @validate_out(instance_of(tuple), Len(l) == 2) def build_house(name, surface=None): print('Building house... DONE !') return name, surface `

Something surprising happens:

`> build_house(None) InputValidationError[TypeError]: name should be a non-empty string.
Error validating input [name=None] ...

build_house('sweet home') # No error ! build_house('sweet home', None) # No error ! `

No error is raised when surface is None, while our base validation functions should both fail in presence of None:

`> is_multiple_of(100)(None)
TypeError: unsupported operand type(s) for %: 'NoneType' and 'int'

(Len(s) > 0).evaluate(None) TypeError: object of type 'NoneType' has no len() `

Indeed, by default None values have a special validation procedure depending on the function signature:

The default behaviour described above may not suit your needs, and can be changed by changing the none_policy keyword argument. Valid values are:

See API reference for usage details, and below validation functions and custom exceptions to explore the various customization possibilities.

c - class fields

If you use pyfields to define your classes, valid8 is already embedded in the field() method: you should rather look here. Otherwise (for normal classes or classes with attrs, autoclass...) continue reading.

The @validate_field decorator adds field validation to a class. It is able to validate a field if the field is:

Here is an example where we add validation to a House class with two fields:

`from valid8 import validate_field from valid8.validation_lib import instance_of, is_multiple_of from mini_lambda import x, s, Len

@validate_field('name', instance_of(str), Len(s) > 0, help_msg='name should be a non-empty string') @validate_field('surface', (x >= 0) & (x < 10000), is_multiple_of(100), help_msg='Surface should be a multiple of100 between 0 and 10000') class House: def init(self, name, surface=None): self.name = name self.surface = surface

@property
def surface(self):
    return self.__surface

@surface.setter
def surface(self, surface=None):
    self.__surface = surface

`

Let's try it:

`> h = House('sweet home') # valid

h = House('') ClassFieldValidationError[ValueError]: name should be a non-empty string.
Error validating field [name=] for class [House].
At least one validation function failed for value ''.
Successes: ["instance_of_<class 'str'>"] / Failures: {'len(s) > 0': 'False'}

h.surface = 10000 ClassFieldValidationError[ValueError]: Surface should be a multiple of 100 between 0 and 10000 Error validating field [surface=10000] for class [House].
At least one validation function failed for value 10000.
Successes: ['is_multiple_of_100']
/ Failures: {'(x >= 0) & (x < 10000)': 'False'}.

h.name = '' # WARNING: does not raise ValidationError (no setter!) `

For classes using attrs or autoclass, it works too.

As for functions, the default none policy is SKIP_IF_NONABLE_ELSE_VALIDATE but can be changed similarly to what we described in previous section

See API reference for usage details, and below validation functions and custom exceptions to explore the various customization possibilities.

2. Validation functions

We have seen above that all entry point validators (inline, function, class) provide a way for you to declare custom validation functions to use. Here are the resources that will get you started:

3. Custom exceptions

Various options are provided to customize the raised exception. These options are available on all entry points: both on validate, validator/validation, @validate_arg, @validate_out, @validate_io and @validate_field.

a - custom message

You can specify a custom error message that will be displayed at the beginning of the default message:

`from valid8 import validation

with validation('surface', surf, help_msg="Surface should be a finite positive integer") as v: v.alid = surf > 0 and isfinite(surf) `

yields for surf = -1

ValidationError[ValueError]: Surface should be a finite positive integer. \ Error validating [surface=-1]: \ validation function [v.alid = surf > 0 and isfinite(surf)] returned [False].

b - custom type

Or even better, a custom error type, which is a good practice to ease internationalization (unique applicative error codes). The base ValidationError class provides default behaviours allowing you to define subclasses in a quite compact form:

`from valid8 import ValidationError, validate

class InvalidSurface(ValidationError): help_msg = 'Surface should be a positive integer'

validate('surface', surf, instance_of=int, min_value=0, error_type=InvalidSurface) `

yields for surf = -1

InvalidSurface[ValueError]: Surface should be a positive integer. \ Error validating [surface=-1]. \ TooSmall: x >= 0 does not hold for x=-1. Wrong value: -1.

Note that for function entry points, the custom type should inherit from InputValidationError or OutputValidationError respectively. For class entry points, the custom type should inherit from ClassFieldValidationError.

c - message templating

Finally, the help_msg field acts as a string template that will receive any additional keyword argument thrown at the validation method. So you can define reusable error classes without too much hassle:

`class InvalidSurface(ValidationError): help_msg = 'Surface should be > {minimum}, found {var_value}'

validate('surface', surf, instance_of=int, min_value=0, error_type=InvalidSurface, minimum=0) `

yields

InvalidSurface[ValueError]: Surface should be > 0, found -1. \ Error validating [surface=-1]. \ TooSmall: x >= 0 does not hold for x=-1. Wrong value: -1.

Note: as shown in that last example, the value being validated is already sent to the help message string to format under the 'var_value' key, so you do not need to pass it.

Obviously you can further customize your exception subclasses if you wish.

d - TypeError or ValueError

ValidationError does not by default inherit from TypeError or ValueError, because in the general case, valid8 has no means of knowing. But you may have noticed in all of the output message examples shown above that one of the two is still appearing each time in the resulting exception. This is because valid8 automatically guesses: when a validation error happens, it recursively looks at the type of the failure cause for a TypeError or ValueError (default if nothing is found). It then dynamically creates an appropriate exception type inheriting both from ValidationError and from either TypeError or ValueError according to what has been found. This dynamic type is cached for speed considerations. It has some custom display methods that make it appear as follows:

`> validate('surface', -1, instance_of=int, min_value=0) ValidationError[ValueError]

validate('surface', 1j, instance_of=int, min_value=0) ValidationError[TypeError] `

If you do not wish the automatic guessing to happen, you can always declare the inheritance explicitly by creating custom subclasses inheriting from the one of your choice, or both:

class InvalidSurface(ValidationError, ValueError): help_msg = ""

If you create a custom subclass but do not inherit from TypeError nor ValueError, the automatic guessing will take place as usual.

Main features

validation functions

Other Validation libraries

Many validation libraries are available on PyPI already. The following list is by far not exhaustive, but gives an overview. Do not hesitate to contribute to this list with a pull request!

Origin of this project

When it all started, I was just looking for a library providing a @validate annotation with a basic library of validators and some logic to associate them, so as to complete autoclass. I searched and found tons of good and less good libraries out there. However none of them was really a good fit for autoclass, for diverse reasons:

So I first created the @validate goodie in autoclass. When the project became more mature, I decided to extract it and make it independent, so that maybe the community will find it interesting.

When this project grew, I found that its embedded library of base validation functions was not flexible enough to cover a large variety of use cases, and will probably never be even if we try our best, so I came up with the complementary mini_lambda syntax which is now an independent project, typically used when your base validation function is very specific, but yet quite simple to write in plain python.

Later on I decided to create a better alternative to autoclass, named pyfields. This challenged the valid8 project once again to be simple, clear, and extensible. This gave version 5.0.0.

See Also

Do you like this library ? You might also like my other python libraries

Want to contribute ?

Details on the github page: https://github.com/smarie/python-valid8