Writing your own validators

After all, using only built-in validators won’t help you much: You’ll need custom validation rules which means that you need to write your own validators.

pycerberus comes with two classes that can serve as a good base when you start writing a custom validator: The BaseValidator only provides the absolutely required set of API so you have maximum freedom. The Validator class itself is inherited from the BaseValidator and defines a more sophisticated API and i18n support. Usually you should use the Validator class.

BaseValidator

class pycerberus.api.BaseValidator(self)

The BaseValidator implements only the minimally required methods. Therefore it does not put many constraints on you. Most users probably want to use the Validator class which already implements some commonly used features.

You can pass messages a dict of messages during instantiation to overwrite messages specified in the validator without the need to create a subclass.

copy()

Return a copy of this instance.

keys()

Return all keys defined by this specific validator class.

message_for_key(key, context)

Return a message for a specific key. Implement this method if you want to avoid calls to messages() which might be costly (otherwise implementing this method is optional).

messages()

Return all messages which are defined by this validator as a key/message dictionary. Alternatively you can create a class-level dictionary which contains these keys/messages.

You must declare all your messages here so that all keys are known after this method was called.

Calling this method might be costly when you have a lot of messages and returning them is expensive. You can reduce the overhead in some situations by implementing message_for_key()

process(value, context=None)

This is the method to validate your input. The validator returns a (Python) representation of the given input value.

In case of errors a InvalidDataError is thrown.

raise_error(key, value, context, errorclass=<class 'pycerberus.errors.InvalidDataError'>, **values)

Raise an InvalidDataError for the given key.

revert_conversion(value, context=None)

Undo the conversion of process() and return a “string-like” representation. This method is especially useful for widget libraries like ToscaWigets so they can render Python data types in a human readable way. The returned value does not have to be an actual Python string as long as it has a meaningful unicode() result. Generally the validator should accept the return value in its ‘.process()’ method.

Validator

class pycerberus.api.Validator(self)

The Validator is the base class of most validators and implements some commonly used features like required values (raise exception if no value was provided) or default values in case no value is given.

This validator splits conversion and validation into two separate steps: When a value is process()``ed, the validator first calls ``convert() which performs some checks on the value and eventually returns the converted value. Only if the value was converted correctly, the validate() function can do additional checks on the converted value and possibly raise an Exception in case of errors. If you only want to do additional checks (but no conversion) in your validator, you can implement validate() and simply assume that you get the correct Python type (e.g. int).

Of course if you can also raise a ValidationError inside of convert() - often errors can only be detected during the conversion process.

By default, a validator will raise an InvalidDataError if no value was given (unless you set a default value). If required is False, the default is None. All exceptions thrown by validators must be derived from ValidationError. Exceptions caused by invalid user input should use InvalidDataError or one of the subclasses.

If strip is True (default is False) and the input value has a strip() method, the input will be stripped before it is tested for empty values and passed to the convert()/validate() methods.

In order to prevent programmer errors, an exception will be raised if you set required to True but provide a default value as well.

convert(value, context)

Convert the input value to a suitable Python instance which is returned. If the input is invalid, raise an InvalidDataError.

empty_value(context)

Return the ‘empty’ value for this validator (usually None).

is_empty(value, context)

Decide if the value is considered an empty value.

messages()

Return all messages which are defined by this validator as a key/message dictionary. Alternatively you can create a class-level dictionary which contains these keys/messages.

You must declare all your messages here so that all keys are known after this method was called.

Calling this method might be costly when you have a lot of messages and returning them is expensive. You can reduce the overhead in some situations by implementing message_for_key()

process(value, context=None)

This is the method to validate your input. The validator returns a (Python) representation of the given input value.

In case of errors a InvalidDataError is thrown.

raise_error(key, value, context, errorclass=<class 'pycerberus.errors.InvalidDataError'>, error_dict=None, error_list=(), **values)

Raise an InvalidDataError for the given key.

validate(converted_value, context)

Perform additional checks on the value which was processed successfully before (otherwise this method is not called). Raise an InvalidDataError if the input data is invalid.

You can implement only this method in your validator if you just want to add additional restrictions without touching the actual conversion.

This method must not modify the converted_value.

Miscellaneous

pycerberus uses a deprecated library called simple_super so you can just say self.super() in your custom validator classes. This will call the super implementation with just the same parameters as your method was called.

Validators need to be thread-safe as one instance might be used several times. Therefore you must not add additional attributes to your validator instance after you called Validator’s constructor. To prevent unexperienced programmers falling in that trap, a ‘’Validator’’ will raise an exception if you try to set an attribute. If you don’t like this behavior and you really know what you are doing, you can issue validator.set_internal_state_freeze(False) to disable that protection.

Putting all together - A simple validator

Now it’s time to put it all together. This validator demonstrates most of the API as explained so far:

class UnicodeValidator(Validator):
    
    def __init__(self, max=None):
        self.super()
        self._max_length = max
    
    def messages(self):
        return {
                'invalid_type': _(u'Validator got unexpected input (expected string, got %(classname)s).'),
                'too_long': _(u'Please enter at maximum %(max_length) characters.')
               }
    # Alternatively you could also declare a class-level variable:
    # messages = {...}
    
    def convert(self, value, context):
        try:
            return unicode(value, 'UTF-8')
        except Exception:
            classname = value.__class__.__name__
            self.raise_error('invalid_type', value, context, classname=classname)
    
    def validate(self, converted_value, context):
        if self._max_length is None:
            return
        if len(converted_value) > self._max_length:
            self.raise_error('too_long', converted_value, context, max_length=self._max_length)

The validator will convert all input to unicode strings (using the UTF-8 encoding). It also checks for a maximum length of the string.

You can see that all the conversion is done in convert() while additional validation is encapsulated in validate(). This can help you keeping your methods small.

In case there is an error the error() method will raise an InvalidDataError. You select the error message to show by passing a string constant key which identifies the message. The key can be used later to adapt the user interface without relying the message itself (e.g. show an additional help box in the user interface if the user typed in the wrong password).

The error messages are declared in the messages(). You’ll notice that the message strings can also contain variable parts. You can use these variable parts to give the user some additional hints about what was wrong with the data.