The following recipe builds on the recipe "Using one method as accessor for multiple attributes" (Python Cookbook, p.752) which in turn seems to be based on http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205126. It adds another argument which provides methods used to validate the value of a property. The interface is modeled after Ian Bicking's FormEncode package (http://formencode.org/). In fact the validators of the FormEncode package can be used as shown in the code below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | import formencode.validators
class Property(object):
def __init__(self,name,fget=getattr,fset=setattr,fdel=delattr,
doc=None,validator=None):
self.name = name
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc or ""
# the validator is an addition to the original recipe.
self.validator = validator
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError,"attribute is write-only"
# from_python()
return self.validator.from_python(self.fget(obj,self.name))
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError,"attribute is read-only"
# to_python()
self.fset(obj, self.name, self.validator.to_python(value))
def __delete__(self,obj):
if self.fdel is None:
raise AttributeError,"attribute cannot be deleted"
self.fdel(obj, self.name)
class IntValidator(object):
"""Validate a properties values.
A validator must provide two methods:
to_python -- used by __set__ to validate/convert the value.
from_python -- used by __get__ to validate/convert the value.
This is modeled after the FormEncode package.
"""
def __init__(self,min=None,max=None):
self.min = min
self.max = max
def to_python(self,value):
"""Validate/convert value before setting the property.
The value must be an integer and between self.min and
self.max inclusive (if not None).
"""
if (self.min is not None) and value < self.min:
raise ValueError,"%s is less than %s" % (value,self.min)
if (self.max is not None) and value > self.max:
raise ValueError,"%s is greater than %s" % (value,self.max)
return int(value)
def from_python(self,value):
"""Validate/convert value when getting it.
Usually we simply return the value as is.
"""
return value
class MyClass(object):
def __init__(self,x,r):
self.x = x
self.r = r
# x must be an integer less than 10
x = Property("_x",validator=IntValidator(max=9))
# r must be an upper case character, we use the FormEncode
# validator here.
r = Property("_r",validator=formencode.validators.Regex(r"^[A-Z]+$"))
if __name__ == "__main__":
c = MyClass(1,"A") # this is fine.
c.x = 9 # this is fine.
c.r = "B" # this is fine.
c.x = 10 # this will raise a ValueError.
c.r = "c" # this will raise formencode.api.Invalid.
|
The naming of the to_python and from_python methods is probably not the best choice but it makes the FormEncode validators fit in nicely. The to_python method does the actual checking of the value, while in most cases from_python will simply return the value as is. Both can be used to convert a value between internal and external representation, compressing and decompressing text for example.