[Zope3-Users] Generic schema validation

Sebastian Bartos seth.kriticos at googlemail.com
Tue Jan 13 17:11:48 EST 2009


Hi folks,

I have written a generic schema validation base class for an external
project of mine (not a Zope 3 instance actually, just use Zope 3
packages).

The basic idea is that it automatically validates changes to an
interface implementation derived from this class and makes a good way of
generic data validation.

It provides validation when a variable is set directly or using the
update function (for several variables).

Now I have a problem with a function that should update several
variables at the same time. Because I can only set one at a time, if an
invariant error occurs (condition of one variable is depending on
another one) the class returns with a half messy state (half of the
variables is set, the other half not).

So is there a way to easily validate the values before I set them (send
them as a list to the validation routine) or make a weird copy of the
class?

Anyway, to show you what I mean, here is the code and a doctest which
shows what should not happen and still does:


Class:

from zope.schema import getValidationErrors as get_validation_errors
from zope.schema import getFieldNames as get_field_names

class schema_base(object):
    """ Base class for interface based schema implementation.
        Provides generic schema validation for schema implementations"""

    field_names = []

    def __init__(self, class_interface):
        """ Set up schema validation.
            Note: to reduce iteration cycles we assume that once a class is
                  initialized based on an interface, the interface won't
                  change anymore.

            Parameters
                class_interface
                We need the interface class we are validating against."""

        self.class_interface = class_interface
        self.field_names = get_field_names(self.class_interface)


    def __setattr__(self, item, value):
        """ The overwriting of this function is required to call the
            schema validation routine when setting attributes directly. 
            
            If the value validation against the interface fails, 
            the old value is reset. """

        if item not in self.field_names and \
            item is not 'class_interface' and \
            item is not 'field_names':
            raise ValueError, "invalid field to set: %s" % item

        item_initialized = False
        
        if hasattr(self, item):
            old_value = self.__getattribute__(item)
            item_initialized = True

        dict.__setattr__(self, item, value)
        
        try:
            get_validation_errors(self.class_interface, self)
        except ValueError:
            if item_initialized is True:
                dict.__setattr__(self, item, old_value)
            raise
        except:
            # TODO: repetition, ugly..
            if item_initialized is True:
                dict.__setattr__(self, item, old_value)
            raise ValueError, "validation of %s failed for value %s" % (
                item, value)
        
    # FIXME: validation is done one by one. if invariant (schema field relative)
    #        errors occur, we get a messy half state.IMPORTANT!!!
    def update(self, **key_value):
        """ With this function we can assing several attributes of the class
            with a single function call. """
    
        for key, value in key_value.items():
            setattr(self, key, value)




And the doctest:

=================
Schema Base Class
=================


Define an interface to base the schema on.

	>>> from zope.interface import Interface, invariant
	>>> from zope.schema import TextLine, Int
	>>> 
	>>> class IData(Interface):
	... 
	...     Name = TextLine(
	...         title = u'Name of the data.', 
	...         readonly = True,
	...         required = True)
	...         
	...     Num = Int(
	...         title = u'Number.',
	...         default = 0,
	...         min = 0, max = 999)
	...         
	...     MaxNum = Int(
	...         title = u'Maximal number.',
	...         description = u'-1 means the number is unknown',
	...         default = -1,
	...         min = -1, max = 999)        
	... 
	...     @invariant
	...     def Num_greater_MaxNum(obj):
	...         if obj.MaxNum is not -1 and obj.MaxNum < obj.Num:
	...             raise ValueError, "%s < %s" % (obj.MaxNum, obj.Num)
	...
	

Connect the interface with an implementation based on the schema base.

	>>> from zope.interface import Interface, implements
	>>> from zope.schema.fieldproperty import FieldProperty
	>>> 
	>>> from generic.schema_base import schema_base
	>>> 
	>>> class DataBase(schema_base):
	...     def __init__(self):
	...         schema_base.__init__(self, IData)
	... 
	>>> 
	>>> class Data(DataBase):
	...     implements(IData)
	... 
	...     Name = FieldProperty(IData['Name'])
	...     Num = FieldProperty(IData['Num'])
	...     MaxNum = FieldProperty(IData['MaxNum'])
	... 
	...     def __init__(self, Name):
	...         DataBase.__init__(self)
	...         self.Name = Name[:255]
	...         

Create an instance of the data class.

    >>> test_class = Data(u'Testclass')

Check supposed values.

    >>> print test_class.Name
    Testclass
    
    >>> print test_class.Num
    0
    
    >>> print test_class.MaxNum
    -1

Set valid values with assigment.

    >>> test_class.Num = 5
    >>> test_class.MaxNum = 10
    
Set valid values with function call.

    >>> test_class.update(Num = 10, MaxNum = -1)
    
Try to set an invalid value with assigment. Should rase exception.

    >>> test_class.Num = -5
    Traceback (most recent call last):
    ...
    TooSmall: (-5, 0)
    
The value shold stayed like it was before failed setting.

    >>> print test_class.Num
    10
    
Do the same with function assigment.

    >>> test_class.update(Num = -5)
    Traceback (most recent call last):
    ...
    TooSmall: (-5, 0)

Check value again.
    
    >>> print test_class.Num
    10
    
Try to set values violating invariant constraints. The error from the invariant
in the interface declaration should be raised.

    >>> test_class.update(Num = 20, MaxNum = 10)    
    Traceback (most recent call last):
    ...
    ValueError: 10 < 20

Values of the test class variables should be unchanged.

!!! Currently this one fails, serious issue! See source!    

    >>> print test_class.Num
    10
    >>> print test_class.MaxNum
    -1

This one should fail.

    >>> test_class.Name = u'Can not rename readonly'
    Traceback (most recent call last):
    ...
    ValueError: ('Name', 'field is readonly')
    
    

(the doctest fails at the point with the !!! marking.)

Now a function like getValidationError(<interface>,
[fieldnames,fieldvalues]) would solve the problem. But I did not find
such a thing.

Any ideas?

Additional comments about the general idea of such a class or other
implementational comments are also welcome.


-- 
Sebastian Bartos, <seth.kriticos at googlemail.com>
keyserevr: pgp.mit.edu
-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 197 bytes
Desc: This is a digitally signed message part
Url : http://mail.zope.org/pipermail/zope3-users/attachments/20090113/28350280/attachment.bin 


More information about the Zope3-users mailing list