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.deferred
class. Instances of thecolander.deferred
class are callables which accept two positional arguments: anode
and akw
dictionary. - When a schema node is bound, it is cloned, and any
colander.deferred
values it has as attributes will be resolved by invoking the callable represented by the deferred value. - A
colander.deferred
value 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.bind
method. For example:someschema.bind(a=1, b=2)
. The keyword values passed tobind
are presented as the valuekw
to eachcolander.deferred
value 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):
= kw.get('max_date')
max_date if max_date is None:
= datetime.date.today()
max_date return colander.Range(min=datetime.date.min, max=max_date)
@colander.deferred
def deferred_date_description(node, kw):
= kw.get('max_date')
max_date if max_date is None:
= datetime.date.today()
max_date return 'Blog post date (no earlier than %s)' % max_date.ctime()
@colander.deferred
def deferred_date_missing(node, kw):
= kw.get('default_date')
default_date if default_date is None:
= datetime.date.today()
default_date return default_date
@colander.deferred
def deferred_body_validator(node, kw):
= kw.get('max_bodylen')
max_bodylen if max_bodylen is None:
= 1 << 18
max_bodylen return colander.Length(max=max_bodylen)
@colander.deferred
def deferred_body_description(node, kw):
= kw.get('max_bodylen')
max_bodylen if max_bodylen is None:
= 1 << 18
max_bodylen return 'Blog post body (no longer than %s bytes)' % max_bodylen
@colander.deferred
def deferred_body_widget(node, kw):
= kw.get('body_type')
body_type if body_type == 'richtext':
= deform.widget.RichTextWidget()
widget else:
= deform.widget.TextAreaWidget()
widget return widget
@colander.deferred
def deferred_category_validator(node, kw):
= kw.get('categories', [])
categories return colander.OneOf([ x[0] for x in categories ])
@colander.deferred
def deferred_category_widget(node, kw):
= kw.get('categories', [])
categories return deform.widget.RadioChoiceWidget(values=categories)
class BlogPostSchema(colander.Schema):
= colander.SchemaNode(
title
colander.String(),= 'Title',
title = 'Blog post title',
description = colander.Length(min=5, max=100),
validator = deform.widget.TextInputWidget(),
widget
)= colander.SchemaNode(
date
colander.Date(),= 'Date',
title = deferred_date_missing,
missing = deferred_date_description,
description = deferred_date_validator,
validator = deform.widget.DateInputWidget(),
widget
)= colander.SchemaNode(
body
colander.String(),= 'Body',
title = deferred_body_description,
description = deferred_body_validator,
validator = deferred_body_widget,
widget
)= colander.SchemaNode(
category
colander.String(),= 'Category',
title = 'Blog post category',
description = deferred_category_validator,
validator = deferred_category_widget,
widget
)
= BlogPostSchema().bind(
schema = datetime.date.max,
max_date = 5000,
max_bodylen = 'richtext',
body_type = datetime.date.today(),
default_date = [('one', 'One'), ('two', 'Two')]
categories )
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
date
node'smissing
value will bedatetime.date.today()
. - The
date
node'svalidator
value will be acolander.Range
validator with amax
ofdatetime.date.max
. - The
date
node'swidget
will be of the typeDateInputWidget
. - The
body
node'sdescription
will be the stringBlog post body (no longer than 5000 bytes)
. - The
body
node'svalidator
value will be acolander.Length
validator with amax
of 5000. - The
body
node'swidget
will be of the typeRichTextWidget
. - The
category
node'svalidator
will be of the typecolander.OneOf
, and itschoices
value will be['one', 'two']
. - The
category
node'swidget
will 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):
= colander.SchemaNode(
title
colander.String(),= 'Title',
title = 'Blog post title',
description = colander.Length(min=5, max=100),
validator = deform.widget.TextInputWidget(),
widget
)= colander.SchemaNode(
date
colander.Date(),= 'Date',
title = 'Date',
description = deform.widget.DateInputWidget(),
widget
)
= BlogPostSchema(after_bind=maybe_remove_date)
blog_schema = blog_schema.bind(use_date=False) blog_schema
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
validator
is deferred,~colander.SchemaNode.deserialize
will raise an~colander.UnboundDeferredError
. - If
missing
is deferred, the field will be considered required. - If
default
is 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.