Types¶
Types are the smallest definition of structure in Schematics. They represent structure by offering functions to inspect or mutate the data in some way.
According to Schematics, a type is an instance of a way to do three things:
- Coerce the data type into an appropriate representation in Python
- Convert the Python representation into other formats suitable for serialization
- Offer a precise method of validating data of many forms
These properties are implemented as to_native
, to_primitive
, and
validate
.
Coercion¶
A simple example is the DateTimeType
.
>>> from schematics.types import DateTimeType
>>> dt_t = DateTimeType()
The to_native
function transforms an ISO8601 formatted date string into a
Python datetime.datetime
.
>>> dt = dt_t.to_native('2013-08-31T02:21:21.486072')
>>> dt
datetime.datetime(2013, 8, 31, 2, 21, 21, 486072)
Conversion¶
The to_primitive
function changes it back to a language agnostic form, in
this case an ISO8601 formatted string, just like we used above.
>>> dt_t.to_primitive(dt)
'2013-08-31T02:21:21.486072'
Validation¶
Validation can be as simple as successfully calling to_native
, but
sometimes more is needed.
data or behavior during a typical use, like serialization.
Let’s look at the StringType
. We’ll set a max_length
of 10.
>>> st = StringType(max_length=10)
>>> st.to_native('this is longer than 10')
u'this is longer than 10'
It converts to a string just fine. Now, let’s attempt to validate it.
>>> st.validate('this is longer than 10')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "schematics/types/base.py", line 164, in validate
raise ValidationError(errors)
schematics.exceptions.ValidationError: [u'String value is too long.']
Custom types¶
If the types provided by the schematics library don’t meet all of your needs,
you can also create new types. Do so by extending
schematics.types.BaseType
, and decide which based methods you need to
override.
to_native¶
By default, this method on schematics.types.BaseType
just returns the
primitive value it was given. Override this if you want to convert it to a
specific native value. For example, suppose we are implementing a type that
represents the net-location portion of a URL, which consists of a hostname and
optional port number:
>>> from schematics.types import BaseType
>>> class NetlocType(BaseType):
... def to_native(self, value):
... if ':' in value:
... return tuple(value.split(':', 1))
... return (value, None)
to_primitive¶
By default, this method on schematics.types.BaseType
just returns the
native value it was given. Override this to convert any non-primitive values to
primitive data values. The following types can pass through safely:
- int
- float
- bool
- basestring
- NoneType
- lists or dicts of any of the above or containing other similarly constrained lists or dicts
To cover values that fall outside of these definitions, define a primitive conversion:
>>> from schematics.types import BaseType
>>> class NetlocType(BaseType):
... def to_primitive(self, value):
... host, port = value
... if port:
... return u'{0}:{1}'.format(host, port)
... return host
validation¶
The base implementation of validate runs individual validators defined:
- At type class definition time, as methods named in a specific way
- At instantiation time as arguments to the type’s init method.
The second type is explained by schematics.types.BaseType
, so we’ll focus
on the first option.
Declared validation methods take names of the form
validate_constraint(self, value), where constraint is an arbitrary name you
give to the check being performed. If the check fails, then the method should
raise schematics.exceptions.ValidationError
:
>>> from schematics.exceptions import ValidationError
>>> from schematics.types import BaseType
>>> class NetlocType(BaseType):
... def validate_netloc(self, value):
... if ':' not in value:
... raise ValidationError('Value must be a valid net location of the form host[:port]')
However, schematics types do define an organized way to define and manage coded error messages. By defining a MESSAGES dict, you can assign error messages to your constraint name. Then the message is available as self.message[‘my_constraint’] in validation methods. Sub-classes can add messages for new codes or replace messages for existing codes. However, they will inherit messages for error codes defined by base classes.
So, to enhance the prior example:
>>> from schematics.exceptions import ValidationError
>>> from schematics.types import BaseType
>>> class NetlocType(BaseType):
... MESSAGES = {
... 'netloc': 'Value must be a valid net location of the form host[:port]'
... }
... def validate_netloc(self, value):
... if ':' not in value:
... raise ValidationError(self.messages['netloc'])
Parameterizing types¶
There may be times when you want to override __init__ and parameterize your type. When you do so, just ensure two things:
Don’t redefine any of the initialization parameters defined for
schematics.types.BaseType
.After defining your specific parameters, ensure that the base parameters are given to the base init method. The simplest way to ensure this is to accept *args and **kwargs and pass them through to the super init method, like so:
>>> from schematics.types import BaseType >>> class NetlocType(BaseType): ... def __init__(self, verify_location=False, *args, **kwargs): ... super(NetlocType, self).__init__(*args, **kwargs) ... self.verify_location = verify_location