[TransWarp] Constraints on model attributes

Roché Compaan roche at upfrontsystems.co.za
Sun Jul 27 16:33:34 EDT 2003

* Phillip J. Eby <pje at telecommunity.com> [2003-07-27 21:22]:
> At 12:43 PM 7/27/03 +0200, Roché Compaan wrote:
> >I've played around more and _onLink and _onUnlink methods are the
> >perfect place for validation and constraint hooks. I don't think
> >anything needs to be done to model features or datatypes. Here's a
> >crude example:
> >
> >class TextLine(model.Attribute):
> >
> >    referencedType = model.String
> >    title = u''
> >    description = u''
> >
> >    def constraint(self, value):
> >        return '\n' not in value and '\r' not in value
> >
> >    def _validate(self, element, item):
> >
> >        if not isinstance(item, unicode):
> >            raise TypeError
> >
> >        if self.constraint is not None and not self.constraint(item):
> >            raise ValidationError
> >
> >    def _onLink(self, element, item, posn):
> >        self._validate(element, item)
> >
> >
> >class Person(model.Element):
> >
> >    class Name(TextLine):
> >        title = u'Name'
> >        description = u'''Person's name'''
> >
> >Do you see any problem with this approach?
> Not as such; but it would be a lot simpler this way:
> class TextLine(model.String):
>     def mdl_normalize(klass, value):
>         value = unicode(value)   # this should raise TypeError if not 
> convertible
>         if '\n' in value or '\r' in value:
>             raise ValueError("Multi-line text")
>         return value
> class Person(model.Element):
>     class Name(model.Attribute):
>         referencedType = TextLine
>         # ...
> This is somewhat more reusable, as well.  I prefer to distinguish between 
> constraints applying to the *type* of the feature, and those that are 
> specific to the feature itself.  The latter kind usually involve conditions 
> imposed by other features of the same object.

The example I posted wasn't showing the simplest way or trying to be
compact but it does provide a hook that makes defining arbitrary
constraints on specific features possible without having to subclass a
model datatype. It also makes sense to specialise model.Attribute if all
features defined on an element must have certain attributes that are
required for form generation eg. title, description, readonly, etc.

I started out following the pattern you describe above but as I went
along I realised some attributes belong in the feature definition and
not in the type definition. I agree with you though, constraints that
have bearing on the type should be defined on the feature type. I
rewrote Zope3's schema fields PEAK style to see how well it will work in
PEAK. The kludgy part at the moment is validating type constraints, but
I think this can be resolved by following the pattern you mention above.

I attach it for your comments.

Roché Compaan
Upfront Systems                 http://www.upfrontsystems.co.za
-------------- next part --------------
from peak.api import *
import errornames
from errors import ValidationError

class Field(model.Attribute):

    title = ''
    description = ''
    readonly = False
    constraint = None
    _type = None

    def _validate(self, value):

        if self._type is not None and not isinstance(value, self._type):
            raise ValidationError(errornames.WrongType, value, self._type)

        if self.constraint is not None and not self.constraint(value):
            raise ValidationError(errornames.ConstraintNotSatisfied, value)

    def _onLink(self, element, item, posn):
        if self.readonly:
            raise TypeError("Can't set values on read-only field %s"
                            % self.__name__)

class Container(Field):

    def _validate(self, value):
        super(Container, self)._validate(value)

        if not hasattr(value, '__contains__'):
            except TypeError:
                raise ValidationError(errornames.NotAContainer, value)

class Iterable(Container):

    def _validate(self, value):
        super(Iterable, self)._validate(value)

        # See if we can get an iterator for it
        except TypeError:
            raise ValidationError(errornames.NotAnIterator, value)

class Orderable(Field):
    """ Values of ordered fields can be sorted.

    They can be restricted to a range of values.

    Orderable is a mixin used in combination with Field.

    min = None
    max = None

    def _validate(self, value):
        super(Orderable, self)._validate(value)

        if self.min is not None and value < self.min:
            raise ValidationError(errornames.TooSmall, value, self.min)

        if self.max is not None and value > self.max:
            raise ValidationError(errornames.TooBig, value, self.max)

class MinMaxLen(Field):
    """ Expresses constraints on the length of a field.
    min_length = 0
    max_length = None

    def _validate(self, value):
        super(MinMaxLen, self)._validate(value)

        if self.min_length is not None and len(value) < self.min_length:
            raise ValidationError(errornames.TooShort, value, self.min_length)

        if self.max_length is not None and len(value) > self.max_length:
            raise ValidationError(errornames.TooLong, value, self.max_length)

class Enumerated(Field):
    """ Enumerated fields can have a value found in a constant set of
        values given by the field definition.

    allowed_values = None

    def _validate(self, value):
        super(Enumerated, self)._validate(value)
        if self.allowed_values:
            if not value in self.allowed_values:
                raise ValidationError(errornames.InvalidValue, value,

class Text(Enumerated, MinMaxLen, Field):
    """A field containing text used for human discourse."""
    _type = unicode

class TextLine(Text):
    """A text field with no newlines."""

    def constraint(self, value):
        return '\n' not in value and '\r' not in value

class EnumeratedTextLine(TextLine):
    """TextLine with a value from a list of allowed values."""

class Password(TextLine):
    """A text field containing a text used as a password."""

class Bool(Field):
    """A field representing a Bool."""
    _type = type(True)

class Int(Enumerated, Orderable, Field):
    """A field representing an Integer."""
    _type = int, long

class EnumeratedInt(Int):
    """A field representing one of a selected set of Integers."""

More information about the PEAK mailing list