Permalink
Browse files

Features

~~~~~~~~

- Calling ``bind`` on a schema node e.g. ``cloned_node = somenode.bind(a=1,
  b=2)`` on a schema node now results in the cloned node having a
  ``bindings`` attribute of the value ``{'a':1, 'b':2}``.

- It is no longer necessary to pass a ``typ`` argument to a SchemaNode
  constructor if the node class has a ``__schema_type__`` callable as a class
  attribute which, when called with no arguments, returns a schema type.
  This callable will be called to obtain the schema type if a ``typ`` is not
  supplied to the constructor.  The default ``SchemaNode`` object's
  ``__schema_type__`` callable raises a ``NotImplementedError`` when it is
  called.

- SchemaNode now has a ``raise_invalid`` method which accepts a message and
  raises a colander.Invalid exception using ``self`` as the node and the
  message as its message.

- It is now possible and advisable to subclass ``SchemaNode`` in order to
  create a bundle of default node behavior.  The subclass can define the
  following methods and attributes: ``preparer``, ``validator``, ``default``,
  ``missing``, ``name``, ``title``, ``description``, ``widget``, and
  ``after_bind``.  For example, the older, more imperative style that
  looked like this still works::

     from colander import SchemaNode

     ranged_int = colander.SchemaNode(
         validator=colander.Range(0, 10),
         default = 10,
         title='Ranged Int'
         )

  But you can alternately now do something like::

     from colander import SchemaNode

     class RangedIntSchemaNode(SchemaNode):
         validator = colander.range(0, 10)
         default = 10
         title = 'Ranged Int'

     ranged_int = RangedInt()

   Values that are expected to be callables can be methods of the schemanode
   subclass instead of plain attributes::

     from colander import SchemaNode

     class RangedIntSchemaNode(SchemaNode):
         default = 10
         title = 'Ranged Int'

         def validator(self, node, cstruct):
            if not 0 < cstruct < 10:
                raise colander.Invalid(node, 'Must be between 0 and 10')

     ranged_int = RangedInt()

   When implementing a method value that expects ``node``, ``node`` must be
   provided in the call signature, even though ``node`` will almost always be
   the same as ``self``.  This is because Colander simply treats the method
   as another kind of callable, be it a method, or a function, or an instance
   that has a ``__call__`` method.  It doesn't care that it happens to be a
   method of ``self``, and it needs to support callables that are not
   methods, so it sends ``node`` in regardless.

   Normal inheritance rules apply to class attributes and methods defined in
   a schemanode subclass.  If your schemanode subclass inherits from another
   schemanode class, your schemanode subclass' methods and class attributes
   will override the superclass' methods and class attributes.

   Method values that need to be deferred for binding cannot currently be
   implemented as ``colander.deferred`` callables.  For example this will
   *not* work::

     from colander import SchemaNode

     class RangedIntSchemaNode(SchemaNode):
         default = 10
         title = 'Ranged Int'

         @colander.deferred
         def validator(self, node, kw):
            request = kw['request']
            def avalidator(node, cstruct):
                if not 0 < cstruct < 10:
                    if request.user != 'admin':
                        raise colander.Invalid(node, 'Must be between 0 and 10')
            return avalidator

     ranged_int = RangedInt()
     bound_ranged_int = ranged_int.bind(request=request)

   This will result in::

        TypeError: avalidator() takes exactly 3 arguments (2 given)

   Instead of trying to defer methods via a decorator, you can instead use
   the ``bindings`` attribute of ``self`` to obtain access to the bind
   parameters within values that are methody::

     from colander import SchemaNode

     class RangedIntSchemaNode(SchemaNode):
         default = 10
         title = 'Ranged Int'

         def validator(self, node, cstruct):
            request = self.bindings['request']
            if not 0 < cstruct < 10:
                if request.user != 'admin':
                    raise colander.Invalid(node, 'Must be between 0 and 10')

     ranged_int = RangedInt()
     bound_range_int = ranged_int.bind(request=request)

   You can use ``after_bind`` to set attributes of the schemanode that rely
   on binding variables, such as ``missing`` and ``default``::

     from colander import SchemaNode

     class RangedIntSchemaNode(SchemaNode):
         default = 10
         title = 'Ranged Int'

         def validator(self, node, cstruct):
            request = self.bindings['request']
            if not 0 < cstruct < 10:
                if request.user != 'admin':
                    raise colander.Invalid(node, 'Must be between 0 and 10')

         def after_bind(self, node, kw):
             self.request = kw['request']
             self.default = self.request.user.id

   Non-method values can still be implemented as ``colander.deferred``
   however::

     from colander import SchemaNode

     def _missing(node, kw):
         request = kw['request']
         if request.user.name == 'admin':
             return 10
          return 20

     class RangedIntSchemaNode(SchemaNode):
         default = 10
         title = 'Ranged Int'
         missing = colander.deferred(_missing)

     ranged_int = RangedInt()

   You can override the default values of a schemanode subclass in its
   constructor::

     from colander import SchemaNode

     class RangedIntSchemaNode(SchemaNode):
         default = 10
         title = 'Ranged Int'
         validator = colander.Range(0, 10)

     ranged_int = RangedInt(validator=colander.Range(0, 20))

   In the above example, the validation will be done on 0-20, not 0-10.

   If your schema node names conflict with schema value attribute names, you
   can work around it with the ``name`` argument to the schema node::

     from colander import SchemaNode, Schema

     class TitleNode(SchemaNode):
         validator = colander.range(0, 10)
         default = 10

     class SomeSchema(Schema):
         title = 'Some Schema'
         thisnamewontmatter = TitleNode(name='title')

Backwards Incompatibilities
~~~~~~~~~~~~~~~~~~~~~~~~~~~

- Passing non-SchemaNode derivative instances as ``*children`` into a
  SchemaNode constructor is no longer supported.  Symptom: ``AttributeError:
  name`` when constructing a SchemaNode.
  • Loading branch information...
1 parent f180c6a commit 84004d0286dd90cc60e1304db42f3724a3715bad @mcdonc mcdonc committed Oct 7, 2012
Showing with 469 additions and 91 deletions.
  1. +186 −0 CHANGES.txt
  2. +107 −77 colander/__init__.py
  3. +2 −2 colander/compat.py
  4. +174 −12 colander/tests/test_colander.py
View
186 CHANGES.txt
@@ -14,6 +14,192 @@ Features
- Add Python 3.3 to tox configuration and use newer tox testing regime
(setup.py dev).
+- Calling ``bind`` on a schema node e.g. ``cloned_node = somenode.bind(a=1,
+ b=2)`` on a schema node now results in the cloned node having a
+ ``bindings`` attribute of the value ``{'a':1, 'b':2}``.
+
+- It is no longer necessary to pass a ``typ`` argument to a SchemaNode
+ constructor if the node class has a ``__schema_type__`` callable as a class
+ attribute which, when called with no arguments, returns a schema type.
+ This callable will be called to obtain the schema type if a ``typ`` is not
+ supplied to the constructor. The default ``SchemaNode`` object's
+ ``__schema_type__`` callable raises a ``NotImplementedError`` when it is
+ called.
+
+- SchemaNode now has a ``raise_invalid`` method which accepts a message and
+ raises a colander.Invalid exception using ``self`` as the node and the
+ message as its message.
+
+- It is now possible and advisable to subclass ``SchemaNode`` in order to
+ create a bundle of default node behavior. The subclass can define the
+ following methods and attributes: ``preparer``, ``validator``, ``default``,
+ ``missing``, ``name``, ``title``, ``description``, ``widget``, and
+ ``after_bind``. For example, the older, more imperative style that
+ looked like this still works::
+
+ from colander import SchemaNode
+
+ ranged_int = colander.SchemaNode(
+ validator=colander.Range(0, 10),
+ default = 10,
+ title='Ranged Int'
+ )
+
+ But you can alternately now do something like::
+
+ from colander import SchemaNode
+
+ class RangedIntSchemaNode(SchemaNode):
+ validator = colander.range(0, 10)
+ default = 10
+ title = 'Ranged Int'
+
+ ranged_int = RangedInt()
+
+ Values that are expected to be callables can be methods of the schemanode
+ subclass instead of plain attributes::
+
+ from colander import SchemaNode
+
+ class RangedIntSchemaNode(SchemaNode):
+ default = 10
+ title = 'Ranged Int'
+
+ def validator(self, node, cstruct):
+ if not 0 < cstruct < 10:
+ raise colander.Invalid(node, 'Must be between 0 and 10')
+
+ ranged_int = RangedInt()
+
+ When implementing a method value that expects ``node``, ``node`` must be
+ provided in the call signature, even though ``node`` will almost always be
+ the same as ``self``. This is because Colander simply treats the method
+ as another kind of callable, be it a method, or a function, or an instance
+ that has a ``__call__`` method. It doesn't care that it happens to be a
+ method of ``self``, and it needs to support callables that are not
+ methods, so it sends ``node`` in regardless.
+
+ Normal inheritance rules apply to class attributes and methods defined in
+ a schemanode subclass. If your schemanode subclass inherits from another
+ schemanode class, your schemanode subclass' methods and class attributes
+ will override the superclass' methods and class attributes.
+
+ Method values that need to be deferred for binding cannot currently be
+ implemented as ``colander.deferred`` callables. For example this will
+ *not* work::
+
+ from colander import SchemaNode
+
+ class RangedIntSchemaNode(SchemaNode):
+ default = 10
+ title = 'Ranged Int'
+
+ @colander.deferred
+ def validator(self, node, kw):
+ request = kw['request']
+ def avalidator(node, cstruct):
+ if not 0 < cstruct < 10:
+ if request.user != 'admin':
+ raise colander.Invalid(node, 'Must be between 0 and 10')
+ return avalidator
+
+ ranged_int = RangedInt()
+ bound_ranged_int = ranged_int.bind(request=request)
+
+ This will result in::
+
+ TypeError: avalidator() takes exactly 3 arguments (2 given)
+
+ Instead of trying to defer methods via a decorator, you can instead use
+ the ``bindings`` attribute of ``self`` to obtain access to the bind
+ parameters within values that are methody::
+
+ from colander import SchemaNode
+
+ class RangedIntSchemaNode(SchemaNode):
+ default = 10
+ title = 'Ranged Int'
+
+ def validator(self, node, cstruct):
+ request = self.bindings['request']
+ if not 0 < cstruct < 10:
+ if request.user != 'admin':
+ raise colander.Invalid(node, 'Must be between 0 and 10')
+
+ ranged_int = RangedInt()
+ bound_range_int = ranged_int.bind(request=request)
+
+ You can use ``after_bind`` to set attributes of the schemanode that rely
+ on binding variables, such as ``missing`` and ``default``::
+
+ from colander import SchemaNode
+
+ class RangedIntSchemaNode(SchemaNode):
+ default = 10
+ title = 'Ranged Int'
+
+ def validator(self, node, cstruct):
+ request = self.bindings['request']
+ if not 0 < cstruct < 10:
+ if request.user != 'admin':
+ raise colander.Invalid(node, 'Must be between 0 and 10')
+
+ def after_bind(self, node, kw):
+ self.request = kw['request']
+ self.default = self.request.user.id
+
+ Non-method values can still be implemented as ``colander.deferred``
+ however::
+
+ from colander import SchemaNode
+
+ def _missing(node, kw):
+ request = kw['request']
+ if request.user.name == 'admin':
+ return 10
+ return 20
+
+ class RangedIntSchemaNode(SchemaNode):
+ default = 10
+ title = 'Ranged Int'
+ missing = colander.deferred(_missing)
+
+ ranged_int = RangedInt()
+
+ You can override the default values of a schemanode subclass in its
+ constructor::
+
+ from colander import SchemaNode
+
+ class RangedIntSchemaNode(SchemaNode):
+ default = 10
+ title = 'Ranged Int'
+ validator = colander.Range(0, 10)
+
+ ranged_int = RangedInt(validator=colander.Range(0, 20))
+
+ In the above example, the validation will be done on 0-20, not 0-10.
+
+ If your schema node names conflict with schema value attribute names, you
+ can work around it with the ``name`` argument to the schema node::
+
+ from colander import SchemaNode, Schema
+
+ class TitleNode(SchemaNode):
+ validator = colander.range(0, 10)
+ default = 10
+
+ class SomeSchema(Schema):
+ title = 'Some Schema'
+ thisnamewontmatter = TitleNode(name='title')
+
+Backwards Incompatibilities
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Passing non-SchemaNode derivative instances as ``*children`` into a
+ SchemaNode constructor is no longer supported. Symptom: ``AttributeError:
+ name`` when constructing a SchemaNode.
+
0.9.9 (2012-09-24)
------------------
View
184 colander/__init__.py
@@ -1540,17 +1540,35 @@ def deserialize(self, node, cstruct):
def timeparse(t, format):
return datetime.datetime(*time.strptime(t, format)[0:6]).time()
-class SchemaNode(object):
+def _add_node_children(node, children):
+ for n in children:
+ insert_before = getattr(n, 'insert_before', None)
+ exists = node.get(n.name, _marker) is not _marker
+ # use exists for microspeed; we could just call __setitem__
+ # exclusively, but it does an enumeration that's unnecessary in the
+ # common (nonexisting) case (.add is faster)
+ if insert_before is None:
+ if exists:
+ node[n.name] = n
+ else:
+ node.add(n)
+ else:
+ if exists:
+ del node[n.name]
+ node.add_before(insert_before, n)
+
+class _SchemaNode(object):
"""
Fundamental building block of schemas.
The constructor accepts these positional arguments:
- - ``typ`` (required): The 'type' for this node. It should be an
+ - ``typ``: The 'type' for this node. It should be an
instance of a class that implements the
- :class:`colander.interfaces.Type` interface.
+ :class:`colander.interfaces.Type` interface. If ``typ`` is not passed,
+ it defaults to ``colander.Mapping()``.
- - ``children``: a sequence of subnodes. If the subnodes of this
+ - ``*children``: a sequence of subnodes. If the subnodes of this
node are not known at construction time, they can later be added
via the ``add`` method.
@@ -1607,29 +1625,49 @@ class SchemaNode(object):
"""
_counter = itertools.count()
-
- def __new__(cls, *arg, **kw):
- inst = object.__new__(cls)
- inst._order = next(cls._counter)
- return inst
-
- def __init__(self, typ, *children, **kw):
- self.typ = typ
- self.preparer = kw.pop('preparer', None)
- self.validator = kw.pop('validator', None)
- self.default = kw.pop('default', null)
- self.missing = kw.pop('missing', required)
- self.name = kw.pop('name', '')
- self.raw_title = kw.pop('title', _marker)
- if self.raw_title is _marker:
- self.title = self.name.replace('_', ' ').title()
+ preparer = None
+ validator = None
+ default = null
+ missing = required
+ name = ''
+ raw_title = _marker
+ title = ''
+ description = ''
+ widget = None
+ after_bind = None
+ bindings = None
+
+ def __new__(cls, *args, **kw):
+ node = object.__new__(cls)
+ node._order = next(cls._counter)
+ node.children = []
+ _add_node_children(node, cls.__all_schema_nodes__)
+ return node
+
+ def __init__(self, *arg, **kw):
+ # bw compat forces us to treat first arg as type always
+ if arg:
+ self.typ = arg[0]
+ _add_node_children(self, arg[1:])
else:
- self.title = self.raw_title
- self.description = kw.pop('description', '')
- self.widget = kw.pop('widget', None)
- self.after_bind = kw.pop('after_bind', None)
+ self.typ = self.__schema_type__()
+
+ # bw compat forces us to manufacture a title if one is not supplied
+ title = kw.get('title', _marker)
+ if title is _marker:
+ name = kw.get('name', self.name)
+ kw['title'] = name.replace('_', ' ').title()
+ else:
+ kw['raw_title'] = title
+
self.__dict__.update(kw)
- self.children = list(children)
+
+ @staticmethod
+ def __schema_type__():
+ raise NotImplementedError(
+ 'Schema node construction without a typ argument or '
+ 'a __schema_node__ callable present on the node class '
+ )
@property
def required(self):
@@ -1787,9 +1825,12 @@ def bind(self, **kw):
return cloned
def _bind(self, kw):
+ self.bindings = kw
for child in self.children:
child._bind(kw)
- for k, v in self.__dict__.items():
+ names = dir(self)
+ for k in names:
+ v = getattr(self, k)
if isinstance(v, deferred):
v = v(self, kw)
setattr(self, k, v)
@@ -1857,77 +1898,66 @@ def __repr__(self):
self.name,
)
+ def raise_invalid(self, msg, node=None):
+ """ Raise a :exc:`colander.Invalid` exception with the message
+ ``msg``. ``node``, if supplied, should be an instance of a
+ :class:`colander.SchemaNode`. If it is not supplied, ``node`` will
+ be this node. Example usage::
+
+ class CustomSchemaNode(SchemaNode):
+ def validator(self, node, cstruct):
+ if cstruct != 'the_right_thing':
+ self.raise_invalid('Not the right thing')
+
+ """
+ if node is None:
+ node = self
+ raise Invalid(node, msg)
+
class _SchemaMeta(type):
def __init__(cls, name, bases, clsattrs):
nodes = []
for name, value in clsattrs.items():
- if isinstance(value, SchemaNode):
+ if isinstance(value, _SchemaNode):
+ delattr(cls, name)
if not value.name:
value.name = name
if value.raw_title is _marker:
value.title = name.replace('_', ' ').title()
nodes.append((value._order, value))
- cls.__schema_nodes__ = nodes
+ cls.__class_schema_nodes__ = nodes
# Combine all attrs from this class and its subclasses.
extended = []
for c in cls.__mro__:
- extended.extend(getattr(c, '__schema_nodes__', []))
+ extended.extend(getattr(c, '__class_schema_nodes__', []))
# Sort the attrs to maintain the order as defined, and assign to the
# class.
extended.sort()
- cls.nodes = [x[1] for x in extended]
-
-def _Schema__new__(cls, *args, **kw):
- node = object.__new__(cls.node_type)
- node.name = None
- node._order = next(SchemaNode._counter)
- typ = cls.schema_type()
- node.__init__(typ, *args, **kw)
- for n in cls.nodes:
- insert_before = getattr(n, 'insert_before', None)
- exists = node.get(n.name, _marker) is not _marker
- # use exists for microspeed; we could just call __setitem__
- # exclusively, but it does an enumeration that's unnecessary in the
- # common (nonexisting) case (.add is faster)
- if insert_before is None:
- if exists:
- node[n.name] = n
- else:
- node.add(n)
- else:
- if exists:
- del node[n.name]
- node.add_before(insert_before, n)
- return node
+ cls.__all_schema_nodes__ = [x[1] for x in extended]
-Schema = _SchemaMeta('Schema', (object,),
- dict(schema_type=Mapping,
- node_type=SchemaNode,
- __new__=_Schema__new__))
+# metaclass spelling compatibility across Python 2 and Python 3
+SchemaNode = _SchemaMeta(
+ 'SchemaNode',
+ (_SchemaNode,),
+ {}
+ )
+
+class Schema(SchemaNode):
+ __schema_type__ = Mapping
MappingSchema = Schema
+class TupleSchema(SchemaNode):
+ __schema_type__ = Tuple
-def _SequenceSchema__new__(cls, *args, **kw):
- node = object.__new__(cls.node_type)
- node.name = None
- node._order = next(SchemaNode._counter)
- typ = cls.schema_type()
- node.__init__(typ, *args, **kw)
- if not len(cls.nodes) == 1:
- raise Invalid(node,
- 'Sequence schemas must have exactly one child node')
- for n in cls.nodes:
- node.add(n)
- return node
-
-SequenceSchema = _SchemaMeta('SequenceSchema', (object,),
- dict(schema_type=Sequence,
- node_type=SchemaNode,
- __new__=_SequenceSchema__new__))
-
-class TupleSchema(Schema):
- schema_type = Tuple
+class SequenceSchema(SchemaNode):
+ __schema_type__ = Sequence
+
+ def __init__(self, *args, **kw):
+ SchemaNode.__init__(self, *args, **kw)
+ if len(self.children) != 1:
+ raise Invalid(self,
+ 'Sequence schemas must have exactly one child node')
class deferred(object):
""" A decorator which can be used to define deferred schema values
View
4 colander/compat.py
@@ -5,7 +5,7 @@
if PY3: # pragma: no cover
string_types = str,
text_type = str
-else:
+else: # pragma: no cover
string_types = basestring,
text_type = unicode
@@ -21,7 +21,7 @@ def is_nonstr_iter(v):
if isinstance(v, str):
return False
return hasattr(v, '__iter__')
-else:
+else: # pragma: no cover
def is_nonstr_iter(v):
return hasattr(v, '__iter__')
View
186 colander/tests/test_colander.py
@@ -2033,47 +2033,52 @@ def test_new_sets_order(self):
self.assertTrue(hasattr(node, '_order'))
def test_ctor_no_title(self):
- node = self._makeOne(None, 0, validator=1, default=2, name='name_a',
+ child = DummySchemaNode(None, name='fred')
+ node = self._makeOne(None, child, validator=1, default=2, name='name_a',
missing='missing')
self.assertEqual(node.typ, None)
- self.assertEqual(node.children, [0])
+ self.assertEqual(node.children, [child])
self.assertEqual(node.validator, 1)
self.assertEqual(node.default, 2)
self.assertEqual(node.missing, 'missing')
self.assertEqual(node.name, 'name_a')
self.assertEqual(node.title, 'Name A')
def test_ctor_with_title(self):
- node = self._makeOne(None, 0, validator=1, default=2, name='name',
+ child = DummySchemaNode(None, name='fred')
+ node = self._makeOne(None, child, validator=1, default=2, name='name',
title='title')
self.assertEqual(node.typ, None)
- self.assertEqual(node.children, [0])
+ self.assertEqual(node.children, [child])
self.assertEqual(node.validator, 1)
self.assertEqual(node.default, 2)
self.assertEqual(node.name, 'name')
self.assertEqual(node.title, 'title')
def test_ctor_with_description(self):
- node = self._makeOne(None, 0, validator=1, default=2, name='name',
+ node = self._makeOne(None, validator=1, default=2, name='name',
title='title', description='desc')
self.assertEqual(node.description, 'desc')
def test_ctor_with_widget(self):
- node = self._makeOne(None, 0, widget='abc')
+ node = self._makeOne(None, widget='abc')
self.assertEqual(node.widget, 'abc')
def test_ctor_with_preparer(self):
- node = self._makeOne(None, 0, preparer='abc')
+ node = self._makeOne(None, preparer='abc')
self.assertEqual(node.preparer, 'abc')
def test_ctor_without_preparer(self):
- node = self._makeOne(None, 0)
+ node = self._makeOne(None)
self.assertEqual(node.preparer, None)
def test_ctor_with_unknown_kwarg(self):
- node = self._makeOne(None, 0, foo=1)
+ node = self._makeOne(None, foo=1)
self.assertEqual(node.foo, 1)
+ def test_ctor_without_type(self):
+ self.assertRaises(NotImplementedError, self._makeOne)
+
def test_required_true(self):
node = self._makeOne(None)
self.assertEqual(node.required, True)
@@ -2367,7 +2372,119 @@ def test_cstruct_children_warning(self):
node = self._makeOne(typ)
self.assertEqual(node.cstruct_children(None), [])
self.assertEqual(len(w), 1)
+
+ def test_raise_invalid(self):
+ import colander
+ typ = DummyType()
+ node = self._makeOne(typ)
+ self.assertRaises(colander.Invalid, node.raise_invalid, 'Wrong')
+
+class TestSchemaNodeSubcassing(unittest.TestCase):
+ def test_subclass_uses_validator_method(self):
+ import colander
+ class MyNode(colander.SchemaNode):
+ __schema_type__ = colander.Int
+ name = 'my'
+ def validator(self, node, cstruct):
+ if cstruct > 10:
+ self.raise_invalid('Wrong')
+ node = MyNode()
+ self.assertRaises(colander.Invalid, node.deserialize, 20)
+
+ def test_subclass_uses_missing(self):
+ import colander
+ class MyNode(colander.SchemaNode):
+ __schema_type__ = colander.Int
+ name = 'my'
+ missing = 10
+ node = MyNode()
+ result = node.deserialize(colander.null)
+ self.assertEqual(result, 10)
+
+ def test_subclass_value_overridden_by_constructor(self):
+ import colander
+ class MyNode(colander.SchemaNode):
+ __schema_type__ = colander.Int
+ name = 'my'
+ missing = 10
+ node = MyNode(missing=5)
+ result = node.deserialize(colander.null)
+ self.assertEqual(result, 5)
+ def test_method_values_can_rely_on_binding(self):
+ import colander
+ class MyNode(colander.SchemaNode):
+ __schema_type__ = colander.Int
+ def amethod(self):
+ return self.bindings['request']
+
+ node = MyNode()
+ newnode = node.bind(request=True)
+ self.assertEqual(newnode.amethod(), True)
+
+ def test_nonmethod_values_can_rely_on_after_bind(self):
+ import colander
+ class MyNode(colander.SchemaNode):
+ __schema_type__ = colander.Int
+ def after_bind(self, node, kw):
+ self.missing = kw['missing']
+
+ node = MyNode()
+ newnode = node.bind(missing=10)
+ self.assertEqual(newnode.deserialize(colander.null), 10)
+
+ def test_deferred_methods_dont_quite_work_yet(self):
+ import colander
+ class MyNode(colander.SchemaNode):
+ __schema_type__ = colander.Int
+ @colander.deferred
+ def avalidator(self, node, kw):
+ def _avalidator(node, cstruct):
+ self.raise_invalid('Foo')
+ return _avalidator
+
+ node = MyNode()
+ self.assertRaises(TypeError, node.bind)
+
+ def test_nonmethod_values_can_be_deferred_though(self):
+ import colander
+ def _missing(node, kw):
+ return 10
+ class MyNode(colander.SchemaNode):
+ __schema_type__ = colander.Int
+ missing = colander.deferred(_missing)
+
+ node = MyNode()
+ bound_node = node.bind()
+ self.assertEqual(bound_node.deserialize(colander.null), 10)
+
+ def test_schema_child_names_conflict_with_value_names_notused(self):
+ import colander
+ def _missing(node, kw):
+ return 10
+ class MyNode(colander.SchemaNode):
+ __schema_type__ = colander.Mapping
+ title = colander.SchemaNode(
+ colander.String(),
+ )
+ node = MyNode()
+ self.assertEqual(node.title, '')
+
+ def test_schema_child_names_conflict_with_value_names_used(self):
+ import colander
+ def _missing(node, kw):
+ return 10
+ doesntmatter = colander.SchemaNode(
+ colander.String(),
+ name='name',
+ )
+ class MyNode(colander.SchemaNode):
+ __schema_type__ = colander.Mapping
+ name = 'fred'
+ wontmatter = doesntmatter
+ node = MyNode()
+ self.assertEqual(node.name, 'fred')
+ self.assertEqual(node['name'], doesntmatter)
class TestMappingSchemaInheritance(unittest.TestCase):
def test_single_inheritance(self):
@@ -2508,7 +2625,7 @@ class MySchema(colander.Schema):
node = MySchema(default='abc')
self.assertTrue(hasattr(node, '_order'))
self.assertEqual(node.default, 'abc')
- self.assertEqual(node.__class__, colander.SchemaNode)
+ self.assertTrue(isinstance(node, colander.SchemaNode))
self.assertEqual(node.typ.__class__, colander.Mapping)
self.assertEqual(node.children[0].typ.__class__, colander.String)
self.assertEqual(node.children[0].title, 'Thing A')
@@ -2535,7 +2652,7 @@ class MySchema(colander.SequenceSchema):
inner = _inner
node = MySchema()
self.assertTrue(hasattr(node, '_order'))
- self.assertEqual(node.__class__, colander.SchemaNode)
+ self.assertTrue(isinstance(node, colander.SchemaNode))
self.assertEqual(node.typ.__class__, colander.Sequence)
self.assertEqual(node.children[0], _inner)
@@ -2567,7 +2684,7 @@ class MySchema(colander.TupleSchema):
thing = colander.SchemaNode(colander.String())
node = MySchema()
self.assertTrue(hasattr(node, '_order'))
- self.assertEqual(node.__class__, colander.SchemaNode)
+ self.assertTrue(isinstance(node, colander.SchemaNode))
self.assertEqual(node.typ.__class__, colander.Tuple)
self.assertEqual(node.children[0].typ.__class__, colander.String)
@@ -2895,6 +3012,51 @@ class MainSchema(colander.MappingSchema):
schema = MainSchema(name=name)
return schema
+class TestUltraDeclarative(unittest.TestCase, TestFunctional):
+
+ def _makeSchema(self, name='schema'):
+
+ import colander
+
+ class IntSchema(colander.SchemaNode):
+ __schema_type__ = colander.Int
+
+ class StringSchema(colander.SchemaNode):
+ __schema_type__ = colander.String
+
+ class TupleSchema(colander.TupleSchema):
+ tupint = IntSchema()
+ tupstring = StringSchema()
+
+ class MappingSchema(colander.MappingSchema):
+ key = IntSchema()
+ key2 = IntSchema()
+
+ class SequenceOne(colander.SequenceSchema):
+ tup = TupleSchema()
+
+ class SequenceTwo(colander.SequenceSchema):
+ mapping = MappingSchema()
+
+ class IntSchemaRanged(IntSchema):
+ validator = colander.Range(0, 10)
+
+ class GlobalObjectSchema(colander.SchemaNode):
+ def __schema_type__(self):
+ return colander.GlobalObject(package=colander)
+
+ class MainSchema(colander.MappingSchema):
+ int = IntSchemaRanged()
+ ob = GlobalObjectSchema()
+ seq = SequenceOne()
+ tup = TupleSchema()
+ seq2 = SequenceTwo()
+
+ MainSchema.name = name
+
+ schema = MainSchema()
+ return schema
+
class Test_null(unittest.TestCase):
def test___nonzero__(self):
from colander import null

0 comments on commit 84004d0

Please sign in to comment.