Files
deb-python-colander/docs/binding.rst
2014-04-14 15:28:49 -04:00

266 lines
9.5 KiB
ReStructuredText

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 :class:`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 the
``colander.deferred`` class. Instances of the ``colander.deferred`` class
are callables which accept two positional arguments: a ``node`` and a
``kw`` 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 :meth:`colander.SchemaNode.bind` method.
For example: ``someschema.bind(a=1, b=2)``. The keyword values
passed to ``bind`` are presented as the value ``kw`` to each
``colander.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:
.. code-block:: python
:linenos:
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 ``date`` node's ``missing`` value will be ``datetime.date.today()``.
- The ``date`` node's ``validator`` value will be a
:class:`colander.Range` validator with a ``max`` of
``datetime.date.max``.
- The ``date`` node's ``widget`` will be of the type ``DateInputWidget``.
- The ``body`` node's ``description`` will be the string ``Blog post
body (no longer than 5000 bytes)``.
- The ``body`` node's ``validator`` value will be a
:class:`colander.Length` validator with a ``max`` of 5000.
- The ``body`` node's ``widget`` will be of the type ``RichTextWidget``.
- The ``category`` node's ``validator`` will be of the type
:class:`colander.OneOf`, and its ``choices`` value will be ``['one',
'two']``.
- The ``category`` node's ``widget`` will be of the type
``RadioChoiceWidget``, 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:
.. code-block:: python
:linenos:
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 :class:`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, :meth:`~colander.SchemaNode.deserialize` will
raise an :exc:`~colander.UnboundDeferredError`.
- If ``missing`` is deferred, the field will be considered *required*.
- If ``default`` is deferred, the serialization default will be
assumed to be ``colander.null``.
See Also
--------
See also the :meth:`colander.SchemaNode.bind` method and the
description of ``after_bind`` in the documentation of the
:class:`colander.SchemaNode` constructor.