Writing Fields¶
Descriptors!¶
The second class in mannequin is the Field
and it is slightly more
interesting than the Model
. It represents the various data attributes of
your objects but does so in an interesting way. The Field
class is what is
called a Descriptor in Python. This is a special object that, when
assigned to the attribute of a class, takes on some special properties.
For a full explanation see the Descriptor How-to Guide
Essentially, a Descriptor is an object that implements a __get__
and a
__set__
method. When a Descriptor instance is assigned to an attribute
of a class, instances of that class will aquire the Descriptor. Because the
attribute is a Descriptor, all access and assignment to that attribute is
controlled by these methods on the Descriptor. Here is a trivial example of
a Descriptor that returns squares of it’s internal value:
class SquaredDescriptor(object):
def __init__(self):
self.__value = None
def __get__(self, obj, obj_type):
try:
return self.__value * self.__value
except TypeError:
return self.__value
def __set__(self, obj, value):
self.__value = value
class Dummy(object):
squared = SquaredDescriptor()
We can see this descriptor in action in the interactive session below:
# first create an instance of the Dummy class
>>> obj = Dummy()
# our instance has the `squared` attribue
>>> print obj.squared
None
# if we assign a numerical value...
>>> obj.squared = 5
# it is squared when accessed
>>> print obj.squared
25
# according to the implementation
>>> obj.squared = "five"
# non-numeric values should be returned as-is
>>> print obj.squared
five
The nature of Descriptors is what makes the Field
class interesting and
useful. Since assignment can be mediated through the Field
it can provide
data sanitation or parsing benefits. The base Field
class has a couple
methods already for implementing such behaviors. Here is the base Field
implementation below:
class Field(object):
defaults = tuple()
def __init__(self, **kwargs):
for (defname, defvalue) in self.defaults:
if defname in kwargs:
setattr(self, defname, kwargs.pop(defname))
if not hasattr(self, defname):
setattr(self, defname, dict(self.defaults)[defname])
if kwargs:
raise TypeError("Unexpected field initialization parameters: " +
', '.join(kwargs.keys()))
def clean(self, value):
return value
def validate(self, value):
pass
def __get__(self, obj, objtype):
return getattr(obj, "__field_{0}".format(id(self)))
def __set__(self, obj, value):
cleaned_value = self.clean(value)
self.validate(cleaned_value)
setattr(obj, '__field_{0}'.format(id(self)), cleaned_value)
When you instantiate a Field
class the first thing that the base implementation does, is loop through the Field.defaults
. This tuple designates what the optional initialization parameters are for the Field
. Any keyword arguments passed to the Field
that it doesn’t expect will raise a TypeError
. Any missing keyword arguments will be given the corresponding default from the defaults
attribute.
The second important thing about the Field
class is that it is a Descriptor. Once instantiated and assigned to a Model
Class declaration, all access and assignment will be regulated by the Field
instance. We can see that the base Field
implementation provides some basic handling here:
def __set__(self, obj, value):
cleaned_value = self.clean(value)
self.validate(cleaned_value)
setattr(obj, '__field_{0}'.format(id(self)), cleaned_value)
When we assign a value to a Field descriptor a few things happen. The first is that the value is passed to Field.clean()
. The default implementation simply returns the value “as is”; however this is a great method in which you can provide your own santiation or other parsing operations. Next, the cleaned value is passed to Field.validate()
. The default implementation here does nothing, but you can stick in your own validation code by overriding the method.
Lastly, once your Field considers the data value as cleaned and validated, the value is stored in a slightly obsfucated manner. Foremost, the value is stored on the *Model instance* that the Field
is bound to. The attribute name given to the value interpolates the Python object ID of the current *Field instance*. The current Field
instance is used so that multiple Fields
of the same type can be bound to the same Model
.
def __get__(self, obj, objtype):
return getattr(obj, "__field_{0}".format(id(self)))
Here we can see that when accessing the Field
value, the same attribute name is generated and the value is returned.
Continuing from here¶
Now that you have a good idea about how both mannequin Models
and Fields
work, head over to the XML Parser Tutorial to see how a real library can be written using mannequin and the declarative technique.