9.5 KiB
Schema Binding
Note
Schema binding is new in colander 0.8.
Sometimes, when you define a schema at module-scope using a
class statement, you simply don't have enough information
to provide fully-resolved arguments to the colander.SchemaNode
constructor. For example, the validator of a schema node
may depend on a set of values that are only available within the scope
of some function that gets called much later in the process lifetime;
definitely some time very much later than module-scope import.
You needn't use schema binding at all to deal with this situation. You can instead mutate a cloned schema object by changing its attributes and assigning it values (such as widgets, validators, etc) within the function which has access to the missing values imperatively within the scope of that function.
However, if you'd prefer, you can use "deferred" values as SchemaNode keyword arguments to a schema defined at module scope, and subsequently use "schema binding" to resolve them later. This can make your schema seem "more declarative": it allows you to group all the code that will be run when your schema is used together at module scope.
What Is Schema Binding?
- Any values passed as a keyword argument to a SchemaNode (e.g.
description,missing, etc.) may be an instance of thecolander.deferredclass. Instances of thecolander.deferredclass are callables which accept two positional arguments: anodeand akwdictionary. - When a schema node is bound, it is cloned, and any
colander.deferredvalues it has as attributes will be resolved by invoking the callable represented by the deferred value. - A
colander.deferredvalue is a callable that accepts two positional arguments: the schema node being bound and a set of arbitrary keyword arguments. It should return a value appropriate for its usage (a widget, a missing value, a validator, etc). - Deferred values are not resolved until the schema is bound.
- Schemas are bound via the
colander.SchemaNode.bindmethod. For example:someschema.bind(a=1, b=2). The keyword values passed tobindare presented as the valuekwto eachcolander.deferredvalue found. - The schema is bound recursively. Each of the schema node's children are also bound.
An Example
Let's take a look at an example:
import datetime
import colander
import deform
@colander.deferred
def deferred_date_validator(node, kw):
max_date = kw.get('max_date')
if max_date is None:
max_date = datetime.date.today()
return colander.Range(min=datetime.date.min, max=max_date)
@colander.deferred
def deferred_date_description(node, kw):
max_date = kw.get('max_date')
if max_date is None:
max_date = datetime.date.today()
return 'Blog post date (no earlier than %s)' % max_date.ctime()
@colander.deferred
def deferred_date_missing(node, kw):
default_date = kw.get('default_date')
if default_date is None:
default_date = datetime.date.today()
return default_date
@colander.deferred
def deferred_body_validator(node, kw):
max_bodylen = kw.get('max_bodylen')
if max_bodylen is None:
max_bodylen = 1 << 18
return colander.Length(max=max_bodylen)
@colander.deferred
def deferred_body_description(node, kw):
max_bodylen = kw.get('max_bodylen')
if max_bodylen is None:
max_bodylen = 1 << 18
return 'Blog post body (no longer than %s bytes)' % max_bodylen
@colander.deferred
def deferred_body_widget(node, kw):
body_type = kw.get('body_type')
if body_type == 'richtext':
widget = deform.widget.RichTextWidget()
else:
widget = deform.widget.TextAreaWidget()
return widget
@colander.deferred
def deferred_category_validator(node, kw):
categories = kw.get('categories', [])
return colander.OneOf([ x[0] for x in categories ])
@colander.deferred
def deferred_category_widget(node, kw):
categories = kw.get('categories', [])
return deform.widget.RadioChoiceWidget(values=categories)
class BlogPostSchema(colander.Schema):
title = colander.SchemaNode(
colander.String(),
title = 'Title',
description = 'Blog post title',
validator = colander.Length(min=5, max=100),
widget = deform.widget.TextInputWidget(),
)
date = colander.SchemaNode(
colander.Date(),
title = 'Date',
missing = deferred_date_missing,
description = deferred_date_description,
validator = deferred_date_validator,
widget = deform.widget.DateInputWidget(),
)
body = colander.SchemaNode(
colander.String(),
title = 'Body',
description = deferred_body_description,
validator = deferred_body_validator,
widget = deferred_body_widget,
)
category = colander.SchemaNode(
colander.String(),
title = 'Category',
description = 'Blog post category',
validator = deferred_category_validator,
widget = deferred_category_widget,
)
schema = BlogPostSchema().bind(
max_date = datetime.date.max,
max_bodylen = 5000,
body_type = 'richtext',
default_date = datetime.date.today(),
categories = [('one', 'One'), ('two', 'Two')]
)We use colander.deferred in its preferred manner here:
as a decorator to a function that takes two arguments. For a schema node
value to be considered deferred, it must be an instance of
colander.deferred and using that class as a decorator is
the easiest way to ensure that this happens.
To perform binding, the bind method of a schema node
must be called. bind returns a clone of the schema
node (and its children, recursively), with all
colander.deferred values resolved. In the above
example:
- The
datenode'smissingvalue will bedatetime.date.today(). - The
datenode'svalidatorvalue will be acolander.Rangevalidator with amaxofdatetime.date.max. - The
datenode'swidgetwill be of the typeDateInputWidget. - The
bodynode'sdescriptionwill be the stringBlog post body (no longer than 5000 bytes). - The
bodynode'svalidatorvalue will be acolander.Lengthvalidator with amaxof 5000. - The
bodynode'swidgetwill be of the typeRichTextWidget. - The
categorynode'svalidatorwill be of the typecolander.OneOf, and itschoicesvalue will be['one', 'two']. - The
categorynode'swidgetwill be of the typeRadioChoiceWidget, and the values it will be provided will be[('one', 'One'), ('two', 'Two')].
after_bind
Whenever a cloned schema node has had its values successfully bound,
it can optionally call an after_bind callback attached to
itself. This can be useful for adding and removing children from schema
nodes:
def maybe_remove_date(node, kw):
if not kw.get('use_date'):
del node['date']
class BlogPostSchema(colander.Schema):
title = colander.SchemaNode(
colander.String(),
title = 'Title',
description = 'Blog post title',
validator = colander.Length(min=5, max=100),
widget = deform.widget.TextInputWidget(),
)
date = colander.SchemaNode(
colander.Date(),
title = 'Date',
description = 'Date',
widget = deform.widget.DateInputWidget(),
)
blog_schema = BlogPostSchema(after_bind=maybe_remove_date)
blog_schema = blog_schema.bind(use_date=False)An after_bind callback is called after a clone of this
node has bound all of its values successfully. The above example removes
the date node if the use_date keyword in the
binding keyword arguments is not true.
The deepest nodes in the node tree are bound first, so the
after_bind methods of the deepest nodes are called before
the shallowest.
An after_bind callback should should accept two values:
node and kw. node will be a clone
of the bound node object, kw will be the set of keywords
passed to the bind method. It usually operates on the
node it is passed using the API methods described in SchemaNode.
Unbound Schemas With Deferreds
If you use a schema with deferred validator,
missing or default attributes, but you use it
to perform serialization and deserialization without calling its
bind method:
- If
validatoris deferred, no validation will be performed. - If
missingis deferred, the field will be considered required. - If
defaultis deferred, the serialization default will be assumed to becolander.null.
See Also
See also the colander.SchemaNode.bind method and the description
of after_bind in the documentation of the colander.SchemaNode
constructor.