Skip to content

Commit

Permalink
- The concept of "schema binding" was added, which allows for a more
Browse files Browse the repository at this point in the history
  declarative-looking spelling of schemas and schema nodes which have
  dependencies on values available after the schema has already been
  fully constructed.  See the new narrative chapter in the
  documentation entitled "Schema Binding".

- The interface of ``colander.SchemaNode`` has grown a ``__delitem__``
  method.  The ``__iter__``, and ``__getitem__`` methods have now also
  been properly documented.
  • Loading branch information
mcdonc committed Sep 8, 2010
1 parent 289ab36 commit 514b06a
Show file tree
Hide file tree
Showing 6 changed files with 412 additions and 19 deletions.
10 changes: 10 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ Next release
- Docstring fixes to ``colander.SchemaNode`` (``missing`` is not the
``null`` value when required, it's a special marker value).

- The concept of "schema binding" was added, which allows for a more
declarative-looking spelling of schemas and schema nodes which have
dependencies on values available after the schema has already been
fully constructed. See the new narrative chapter in the
documentation entitled "Schema Binding".

- The interface of ``colander.SchemaNode`` has grown a ``__delitem__``
method. The ``__iter__``, and ``__getitem__`` methods have now also
been properly documented.

0.7.3 (2010/09/02)
------------------

Expand Down
95 changes: 76 additions & 19 deletions colander/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
_marker = object()

class _null(object):
""" Represents a null value in colander-related operations. """
def __nonzero__(self):
return False

Expand Down Expand Up @@ -943,7 +944,7 @@ def _zope_dottedname_style(self, node, value):
used += '.' + n
try:
found = getattr(found, n)
except AttributeError:
except AttributeError: # pragma: no cover
__import__(used)
found = getattr(found, n)

Expand Down Expand Up @@ -1160,6 +1161,20 @@ class SchemaNode(object):
an object that implements the
:class:`colander.interfaces.Validator` interface.
- ``after_bind``: A callback which is called after a clone of this
node has 'bound' all of its values successfully. This callback
is useful for performing arbitrary actions to the cloned node,
or direct children of the cloned node (such as removing or
adding children) at bind time. A 'binding' is the result of an
execution of the ``bind`` method of the clone's prototype node,
or one of the parents of the clone's prototype nodes. The
deepest nodes in the node tree are bound first, so the
``after_bind`` methods of the deepest nodes are called before
the shallowest. The ``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.
- ``title``: The title of this node. Defaults to a titleization
of the ``name`` (underscores replaced with empty strings and the
first letter of every resulting word capitalized). The title is
Expand All @@ -1172,6 +1187,7 @@ class SchemaNode(object):
- ``widget``: The 'widget' for this node. Defaults to ``None``.
The widget attribute is not interpreted by Colander itself, it
is only meaningful to higher-level systems such as Deform.
"""

_counter = itertools.count()
Expand All @@ -1190,22 +1206,11 @@ def __init__(self, typ, *children, **kw):
self.title = kw.pop('title', self.name.replace('_', ' ').title())
self.description = kw.pop('description', '')
self.widget = kw.pop('widget', None)
self.after_bind = kw.pop('after_bind', None)
if kw:
raise TypeError('Unknown keyword arguments: %s' % repr(kw))
self.children = list(children)

def __iter__(self):
""" Iterate over the children nodes of this schema node """
return iter(self.children)

def __repr__(self):
return '<%s.%s object at %d (named %s)>' % (
self.__module__,
self.__class__.__name__,
id(self),
self.name,
)

@property
def required(self):
""" A property which returns ``True`` if the ``missing`` value
Expand Down Expand Up @@ -1268,12 +1273,6 @@ def add(self, node):
""" Add a subnode to this node. """
self.children.append(node)

def __getitem__(self, name):
for node in self.children:
if node.name == name:
return node
raise KeyError(name)

def clone(self):
""" Clone the schema node and return the clone. All subnodes
are also cloned recursively. Attributes present in node
Expand All @@ -1283,6 +1282,54 @@ def clone(self):
cloned.children = [ node.clone() for node in self.children ]
return cloned

def bind(self, **kw):
""" Resolve any deferred values attached to this schema node
and its children (recursively), using the keywords passed as
``kw`` as input to each deferred value. This function
*clones* the schema it is called upon and returns the cloned
value. The original schema node (the source of the clone)
is not modified."""
cloned = self.clone()
cloned._bind(kw)
return cloned

def _bind(self, kw):
for child in self.children:
child._bind(kw)
for k, v in self.__dict__.items():
if isinstance(v, deferred):
v = v(self, kw)
setattr(self, k, v)
if getattr(self, 'after_bind', None):
self.after_bind(self, kw)

def __delitem__(self, name):
""" Remove a subnode by name """
for idx, node in enumerate(self.children[:]):
if node.name == name:
return self.children.pop(idx)
raise KeyError(name)

def __getitem__(self, name):
""" Get a subnode by name. """
for node in self.children:
if node.name == name:
return node
raise KeyError(name)

def __iter__(self):
""" Iterate over the children nodes of this schema node """
return iter(self.children)

def __repr__(self):
return '<%s.%s object at %d (named %s)>' % (
self.__module__,
self.__class__.__name__,
id(self),
self.name,
)


class _SchemaMeta(type):
def __init__(cls, name, bases, clsattrs):
nodes = []
Expand Down Expand Up @@ -1339,3 +1386,13 @@ def __new__(cls, *args, **kw):

class TupleSchema(Schema):
schema_type = Tuple

class deferred(object):
""" A decorator which can be used to define deferred schema values
(missing values, widgets, validators, etc.)"""
def __init__(self, wrapped):
self.wrapped = wrapped

def __call__(self, node, kw):
return self.wrapped(node, kw)

76 changes: 76 additions & 0 deletions colander/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,17 @@ def test___getitem__failure(self):
node = self._makeOne(None)
self.assertRaises(KeyError, node.__getitem__, 'another')

def test___delitem__success(self):
node = self._makeOne(None)
another = self._makeOne(None, name='another')
node.add(another)
del node['another']
self.assertEqual(node.children, [])

def test___delitem__failure(self):
node = self._makeOne(None)
self.assertRaises(KeyError, node.__delitem__, 'another')

def test___iter__(self):
node = self._makeOne(None)
node.children = ['a', 'b', 'c']
Expand All @@ -1435,6 +1446,71 @@ def test_clone(self):
self.assertEqual(inner_clone.name, 'inner')
self.assertEqual(inner_clone.foo, 2)

def test_bind(self):
from colander import deferred
inner_typ = DummyType()
outer_typ = DummyType()
def dv(node, kw):
self.failUnless(node.name in ['outer', 'inner'])
self.failUnless('a' in kw)
return '123'
dv = deferred(dv)
outer_node = self._makeOne(outer_typ, name='outer', missing=dv)
inner_node = self._makeOne(inner_typ, name='inner', validator=dv,
missing=dv)
outer_node.children = [inner_node]
outer_clone = outer_node.bind(a=1)
self.failIf(outer_clone is outer_node)
self.assertEqual(outer_clone.missing, '123')
inner_clone = outer_clone.children[0]
self.failIf(inner_clone is inner_node)
self.assertEqual(inner_clone.missing, '123')
self.assertEqual(inner_clone.validator, '123')

def test_bind_with_after_bind(self):
from colander import deferred
inner_typ = DummyType()
outer_typ = DummyType()
def dv(node, kw):
self.failUnless(node.name in ['outer', 'inner'])
self.failUnless('a' in kw)
return '123'
dv = deferred(dv)
def remove_inner(node, kw):
self.assertEqual(kw, {'a':1})
del node['inner']
outer_node = self._makeOne(outer_typ, name='outer', missing=dv,
after_bind=remove_inner)
inner_node = self._makeOne(inner_typ, name='inner', validator=dv,
missing=dv)
outer_node.children = [inner_node]
outer_clone = outer_node.bind(a=1)
self.failIf(outer_clone is outer_node)
self.assertEqual(outer_clone.missing, '123')
self.assertEqual(len(outer_clone.children), 0)
self.assertEqual(len(outer_node.children), 1)

class TestDeferred(unittest.TestCase):
def _makeOne(self, wrapped):
from colander import deferred
return deferred(wrapped)

def test_ctor(self):
wrapped = '123'
inst = self._makeOne(wrapped)
self.assertEqual(inst.wrapped, wrapped)

def test___call__(self):
n = object()
k = object()
def wrapped(node, kw):
self.assertEqual(node, n)
self.assertEqual(kw, k)
return 'abc'
inst = self._makeOne(wrapped)
result= inst(n, k)
self.assertEqual(result, 'abc')

class TestSchema(unittest.TestCase):
def test_alias(self):
from colander import Schema
Expand Down
9 changes: 9 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ Schema-Related
.. autoclass:: SchemaNode
:members:

.. automethod:: __delitem__

.. automethod:: __getitem__

.. automethod:: __iter__

.. autoclass:: Schema

.. autoclass:: MappingSchema
Expand All @@ -114,3 +120,6 @@ Schema-Related

.. attribute:: null

Represents a null value in colander-related operations.


0 comments on commit 514b06a

Please sign in to comment.