Mappers¶
As the name suggests, a Mapper will map properties on themselves to your object. They allow you to easily write proxy objects, primarily for converting between serialised (JSON) and live (Python) formats of your resources.
Warning
Since a Mapper instance retains a reference to the object they are bound to, even when using << syntax, instances MUST NOT be shared between threads.
field decorator: get/set¶
Mappers work using Python’s descriptor protocol, which is most commonly used
via the property
built-in. This gives you full control over a Mapper’s
properties. When constructing a Mapper you can pass an object for it to “bind”
to. All attribute access to the Mapper fields will proxy to this bound object.
Here’s an example to illustrate some of these concepts:
# An object we want to create a Mapper for
class Person:
def __init__(self, first_name, last_name, is_alive):
self.first_name = first_name
self.last_name = last_name
self.is_alive = is_alive
from nap import mapper
# A Mapper that we are creating for the Person object
class PersonMapper(mapper.Mapper):
'''
The self argument refers to the object we bind the Mapper to when we
construct it. It DOES NOT refer to the instance of the PersonMapper.
'''
@mapper.field
def name(self):
return '{}'.format(self.first_name, self.last_name)
# We can use the Field class for simpler cases
first_name = mapper.Field('first_name')
last_name = mapper.Field('last_name')
is_alive = mapper.Field('is_alive')
# Construct instances of the Person and a Mapper classes
person = Person('Jane', 'Doe', 22, True)
mapper = PersonMapper(person)
See `Fields`_ for more details.
Mapper functions¶
A Mapper supports several methods:
_reduce()
will reduce the instance to its serialisable state, returning a
dict representation of the Mapper.
_patch(data)
will partially update (patch) a Mapper’s fields with the
values you pass in the data dict. If validation fails it will raise a
ValidationError.
_apply(data)
will fully update (put) a Mapper’s fields with the values
you pass in the data dict. If you don’t pass a field in the data dict it will
try to set the field to the default value. If there is no default and the field
is required it will raise a ValidationError.
_clean(data, full=True)
is a hook for final pass validation. It allows you
to define your own custom cleaning code. You should update the self._errors
dict. The full
boolean indicates if the calling method was _apply
(True) or _patch
(False).
Here is some code to explain how these concepts work. We will continue to use the Person class and PersonMapper class defined above.
Note that these methods only update the fields of the model instance. You will need to call save() yourself to commit changes to the database.
Using _reduce:
p = Person('Jane', 'Doe', True)
m = PersonMapper(p)
reduced_p = m._reduce()
print(reduced_p)
# Output: {'first_name': 'Jane', 'last_name': 'Doe', 'is_alive': True}
Using _apply:
m = PersonMapper()
m._apply({
"first_name": "Jane",
"last_name": "Doe",
"is_alive": False
})
reduced = m.reduce()
print(reduced)
# Output: {'first_name': 'Jane', 'last_name': 'Doe', 'is_alive': False}
Using _patch:
p = Person('Jane', 'Doe', True)
m = PersonMapper(p)
m._patch({"last_name": "Notdoe"}) # This should patch last_name
reduced = m.reduce()
print(reduced)
# Output: {'first_name': 'Jane', 'last_name': 'Notdoe', 'is_alive': True}
Using _clean:
class DeadPersonMapper(PersonMapper):
def _clean(self):
if self.is_alive:
raise ValidationError("Only dead people accepted to the morgue.")
m = DeadPersonMapper()
m._apply({'last_name': 'Doe', 'first_name': 'John', 'is_alive': True})
# ValidationError
Shortcuts¶
As a convenience, Mappers support two shorthand syntaxes:
>>> data = mapper << obj
This will bind the mapper to the obj, and then call _reduce
.
>>> obj = data >> mapper
This will call _patch
on the mapper, passing data, and returning the
updated object.
ModelMappers¶
A ModelMapper
will automatically create a Mapper
for a Django model. A
ModelMapper
behaves very similar to a Django ModelForm
, you control it
by setting some fields in an inner Meta
class.
The fields that can be set are:
-
class
Meta
¶ -
model
¶ Default: None
The model this
Mapper
is for
-
fields
¶ Default: []
The list of fields to use. You can set it to ‘__all__’ to map all fields.
-
exclude
¶ Default: []
The list of fields to exclude from the Model
-
required
¶ Default: {}
A map to override required values for fields auto-created from the Model.
-
readonly
¶ Default: []
The list of fields which are read only.
Must not conflict with required.
-
You can rewrite the Mapper so that it subclasses ModelMapper. Here’s a new Person object that subclasses Django’s models.Model:
from django.db import models
# An Django models.Model we want to create a Mapper for
class Person(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
is_alive = models.BooleanField(default=True)
Here is the PersonMapper rewritten to use a ModelMapper:
from nap import mapper
# This should reference the model package where we define Person
from . import models
class PersonMapper(mapper.ModelMapper):
class Meta:
model = models.Person
fields = '__all__'
You can still use field to get/set properties and fields on a ModelMapper. This is useful when the model contains some properties that the ModelMapper cannot understand, or when you want to customise how certain fields are represented.
To illustrate this we will add a new Django field (models.UUIDField) to our model. UUIDField does not have a filter built in to nap, so you will need to define your own get and set functionality using the field decorator.
Here is a Person model object with a UUIDField:
from django.db import models
# An Django models.Model we want to create a Mapper for
class Person(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
is_alive = models.BooleanField(default=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
And here is a complete ModelMapper that will correctly handle this new type of field:
from nap import mapper
from . import models
class PersonMapper(mapper.ModelMapper):
class Meta:
model = models.Person
fields = '__all__'
@mapper.field(readonly=True)
def uuid(self):
return str(self.uuid) # Remember: self refers to the bound object.