Commits

Chris McDonough  committed 4274397

svn merge -r9524:9564 $REPOZE_SVN/deform/branches/default_overhaul

  • Participants
  • Parent commits ee1c29f

Comments (0)

Files changed (22)

 - Change default form action to the empty string (rather than ``.``).
   Thanks to Kiran.
 
+- Custom widgets must now check for ``colander.null`` rather than
+  ``None`` as the null sentinel value.
+
+- Dependency on a new (default_overhaul) version of Colander.
+
 0.2 (2010-05-13)
 ----------------
 

File deform/field.py

     required
         An alias for self.schema.required
 
-    default
-        An alias for self.schema.sdefault
-
     typ
         An alias for self.schema.typ
 
             widget_maker = widget.TextInputWidget
         return widget_maker()
 
-    @decorator.reify
-    def default(self):
-        """ The serialized schema default """
-        return self.schema.sdefault
-
     @property
     def errormsg(self):
         """ Return the ``msg`` attribute of the ``error`` attached to
         the return value will be ``None``."""
         return getattr(self.error, 'msg', None)
 
-    def render(self, appstruct=None, readonly=False):
+    def serialize(self, cstruct, readonly=False):
+        """ Serialize the cstruct into HTML.  If ``readonly`` is
+        ``True``, render a read-only rendering (no input fields)."""
+        return self.widget.serialize(self, cstruct=cstruct, readonly=readonly)
+
+    def deserialize(self, pstruct):
+        """ Deserialize the pstruct into a cstruct."""
+        return self.widget.deserialize(self, pstruct)
+
+    def render(self, appstruct=colander.null, readonly=False):
         """ Render the field (or form) to HTML using ``appstruct`` as
         a set of default values.  ``appstruct`` is typically a
         dictionary of application values matching the schema used by
         :meth:`colander.SchemaNode.serialize` and
         :meth:`deform.widget.Widget.serialize` .
         """
-        if appstruct is None:
-            appstruct = {}
         cstruct = self.schema.serialize(appstruct)
-        return self.widget.serialize(self, cstruct, readonly=readonly)
+        return self.serialize(cstruct, readonly=readonly)
 
     def validate(self, controls):
         """
         e = None
 
         try:
-            cstruct = self.widget.deserialize(self, pstruct)
+            cstruct = self.deserialize(pstruct)
         except colander.Invalid, e:
             # fill in errors raised by widgets
             self.widget.handle_error(self, e)
             raise exception.ValidationFailure(self, cstruct, e)
 
         return appstruct
+
+    def __repr__(self):
+        return '<%s.%s object at %d (schemanode %r)>' % (
+            self.__module__,
+            self.__class__.__name__,
+            id(self),
+            self.schema.name,
+            )

File deform/schema.py

         ``mimetype`` is not provided, the widget will not be able to
         display mimetype information.
         """
+        if value is colander.null:
+            return colander.null
+        
         if not hasattr(value, 'get'):
             mapping = {'value':repr(value)}
             raise colander.Invalid(
         return result
 
     def deserialize(self, node, value):
-        if not value and node.required:
-            raise colander.Invalid(node, _('Required'))
+        if value is colander.null:
+            return colander.null
         return value
 
 class Set(object):
     """ A type representing a non-overlapping set of items.
-    Deserializes an iterable to a ``set`` object. """
+    Deserializes an iterable to a ``set`` object.
+
+    This type constructor accepts one argument:
+
+    ``allow_empty``
+       Boolean representing whether an empty set input to
+       deserialize will be considered valid.  Default: ``False``.
+    """
+
     widget_maker = widget.CheckboxChoiceWidget
+
+    def __init__(self, allow_empty=False):
+        self.allow_empty = allow_empty
+        
+    def serialize(self, node, value):
+        if value is colander.null:
+            return colander.null
+        return value
+
     def deserialize(self, node, value):
+        if value is colander.null:
+            return colander.null
         if not hasattr(value, '__iter__'):
             raise colander.Invalid(
                 node,
                 _('${value} is not iterable', mapping={'value':value})
                 )
-        if not value:
-            if node.required:
-                raise colander.Invalid(node, _('Required'))
-            value = node.default
-        return set(value)
+        value =  set(value)
+        if not value and not self.allow_empty:
+            raise colander.Invalid(node, _('Required'))
+        return value
+    
+        
 
-    def serialize(self, node, value):
-        return value
-

File deform/templates/form.pt

                       tmpl field.widget.item_template"
           tal:repeat="f field.children"
           tal:replace="structure 
-                       rndr(tmpl,field=f,cstruct=cstruct.get(f.name))"/>
+                       rndr(tmpl,field=f,cstruct=cstruct.get(f.name, null))"/>
       
       <li class="buttons">
         <tal:block repeat="button field.buttons">

File deform/templates/mapping.pt

        tal:define="rndr field.renderer;
                    tmpl field.widget.item_template"
        tal:repeat="f field.children"
-       tal:replace="structure rndr(tmpl,field=f,cstruct=cstruct.get(f.name))"/>
+       tal:replace="structure rndr(tmpl,field=f,cstruct=cstruct.get(f.name,null))"/>
     <input type="hidden" name="__end__" value="${field.name}:mapping"/>
   </ul>
   <!-- /mapping -->

File deform/templates/mapping_item.pt

          >${field.title}<span tal:condition="field.required" class="req"
                               id="req-${field.oid}">*</span>
   </label>
-  <span tal:replace="structure field.widget.serialize(field, cstruct)"/>
+  <span tal:replace="structure field.serialize(cstruct)"/>
   <p tal:condition="field.error and not field.widget.hidden"
      id="error-${field.oid}"
      class="${field.widget.error_class}">${field.error.msg}</p>

File deform/templates/readonly/form.pt

                   tmpl field.widget.readonly_item_template"
       tal:repeat="f field.children"
       tal:replace="structure 
-                  rndr(tmpl,field=f,cstruct=cstruct.get(f.name))"/>
+                  rndr(tmpl,field=f,cstruct=cstruct.get(f.name, null))"/>
   
 </div>

File deform/templates/readonly/mapping.pt

                      tmpl field.widget.readonly_item_template"
          tal:repeat="f field.children"
          tal:replace="structure
-                        rndr(tmpl,field=f,cstruct=cstruct.get(f.name))"/>
+                        rndr(tmpl,field=f,cstruct=cstruct.get(f.name, null))"/>
   </ul>
   <!-- /mapping -->
 </div>

File deform/templates/readonly/mapping_item.pt

          title="${field.description}"
          >${field.title}</p>
   <span tal:replace="structure
-            field.widget.serialize(field, cstruct, readonly=True)"/>
+            field.serialize(cstruct, readonly=True)"/>
   <!-- /mapping_item -->
 </li>

File deform/templates/readonly/sequence_item.pt

     tal:omit-tag="field.widget.hidden">
   <!-- sequence_item -->
   <span tal:replace="structure
-        field.widget.serialize(field, cstruct, readonly=True)"/>
+        field.serialize(cstruct, readonly=True)"/>
   <!-- /sequence_item -->
 </li>

File deform/templates/sequence_item.pt

        src="${parent.widget.closebutton_url}" 
        alt="Remove" onclick="javascript:$(this).parent().remove();"/>
 
-  <span tal:replace="structure field.widget.serialize(field, cstruct)"/>
+  <span tal:replace="structure field.serialize(cstruct)"/>
   <p tal:condition="field.error and not field.widget.hidden"
      id="error-${field.oid}"
      class="${field.widget.error_class}">${field.error.msg}</p>

File deform/tests/test_field.py

         self.assertEqual(field.renderer, default_renderer)
         self.assertEqual(field.name, 'name')
         self.assertEqual(field.title, 'title')
-        self.assertEqual(field.default, 'sdefault')
         self.assertEqual(field.required, True)
         self.assertEqual(field.order, 0)
         self.assertEqual(field.oid, 'deformField0')
         self.assertEqual(child_field.schema, node)
         self.assertEqual(child_field.renderer, 'abc')
 
-    def test_ctor_schema_required(self):
-        schema = DummySchema()
-        schema.required = False
-        field = self._makeOne(schema)
-        self.assertEqual(field.default, 'sdefault')
-
     def test_set_default_renderer(self):
         cls = self._getTargetClass()
         old = cls.default_renderer
         self.assertEqual(field.render('abc', readonly=True), 'abc')
         self.assertEqual(widget.rendered, 'readonly')
 
+    def test_serialize(self):
+        schema = DummySchema()
+        field = self._makeOne(schema)
+        widget = field.widget = DummyWidget()
+        self.assertEqual(field.serialize('abc'), 'abc')
+        self.assertEqual(widget.rendered, 'writable')
 
+    def test_serialize_null(self):
+        from colander import null
+        schema = DummySchema()
+        field = self._makeOne(schema)
+        widget = field.widget = DummyWidget()
+        self.assertEqual(field.serialize(null), null)
+        self.assertEqual(widget.rendered, 'writable')
+
+    def test_deserialize(self):
+        cstruct = {'name':'Name', 'title':'Title'}
+        schema = DummySchema()
+        field = self._makeOne(schema)
+        field.widget = DummyWidget()
+        result = field.deserialize(cstruct)
+        self.assertEqual(result, {'name':'Name', 'title':'Title'})
+
+    def test___repr__(self):
+        schema = DummySchema()
+        field = self._makeOne(schema)
+        r = repr(field)
+        self.failUnless(r.startswith('<deform.field.Field object at '))
+        self.failUnless(r.endswith("(schemanode 'name')>"))
 
 class DummyField(object):
     name = 'name'

File deform/tests/test_functional.py

         class MySchema(MappingSchema):
             name = SchemaNode(String())
             title = SchemaNode(String())
-            cool = SchemaNode(Boolean(), default=True)
+            cool = SchemaNode(Boolean(), default=True, missing=True)
             series = SeriesSchema()
 
         schema = MySchema()
         self.assertEqual(result, expected)
 
     def test_validate(self):
+        from colander import null
         from deform.exception import ValidationFailure
         schema = self._makeSchema()
         form = self._makeForm(schema)
                          e.children[2].children[0])
         self.assertEqual(
             ve.cstruct,
-            {'series': {'dates': [], 'name': ''}, 'cool': 'false', 'name': '',
-             'title': ''})
+            {
+                'series': {'dates': [], 'name': null},
+                'cool': 'false',
+                'name': null,
+                'title': null,
+             }
+            )
         
         
         

File deform/tests/test_schema.py

         result = typ.serialize(node, provided)
         self.failUnless(result is provided)
 
+    def test_serialize_null(self):
+        from colander import null
+        node = DummySchemaNode()
+        typ = self._makeOne()
+        result = typ.serialize(node, null)
+        self.assertEqual(result, null)
+
     def test_deserialize_no_iter(self):
         node = DummySchemaNode()
         typ = self._makeOne()
         e = invalid_exc(typ.deserialize, node, 'str')
         self.assertEqual(e.msg, '${value} is not iterable')
 
-    def test_deserialize_empty_required_no_default(self):
+    def test_deserialize_null(self):
+        from colander import null
         node = DummySchemaNode()
         typ = self._makeOne()
-        e = invalid_exc(typ.deserialize, node, ())
-        self.assertEqual(e.msg, 'Required')
-
-    def test_deserialize_empty_required_with_default(self):
-        node = DummySchemaNode()
-        typ = self._makeOne()
-        node.default = ('abc',)
-        node.required = False
-        result = typ.deserialize(node, ())
-        self.assertEqual(result, set(('abc',)))
+        result = typ.deserialize(node, null)
+        self.assertEqual(result, null)
 
     def test_deserialize_valid(self):
         node = DummySchemaNode()
         result = typ.deserialize(node, ('a',))
         self.assertEqual(result, set(('a',)))
 
+    def test_deserialize_empty_allow_empty_false(self):
+        node = DummySchemaNode()
+        typ = self._makeOne()
+        e = invalid_exc(typ.deserialize, node, ())
+        self.assertEqual(e.msg, 'Required')
+
+    def test_deserialize_empty_allow_empty_true(self):
+        node = DummySchemaNode()
+        typ = self._makeOne(allow_empty=True)
+        result = typ.deserialize(node, ())
+        self.assertEqual(result, set())
+
 class TestFileData(unittest.TestCase):
     def _makeOne(self):
         from deform.schema import FileData
         return FileData()
 
-    def test_deserialize_required_None(self):
+    def test_deserialize_null(self):
+        from colander import null
         typ = self._makeOne()
         node = DummySchemaNode()
-        e = invalid_exc(typ.deserialize, node, None)
-        self.assertEqual(e.msg, 'Required')
-        
-    def test_deserialize_notrequired_None(self):
-        typ = self._makeOne()
-        node = DummySchemaNode()
-        node.required = False
-        result = typ.deserialize(node, None)
-        self.assertEqual(result, None)
+        result = typ.deserialize(node, null)
+        self.assertEqual(result, null)
 
-    def test_deserialize_not_None(self):
+    def test_deserialize_not_null(self):
         typ = self._makeOne()
         node = DummySchemaNode()
         result = typ.deserialize(node, '123')
         self.assertEqual(result, '123')
 
+    def test_serialize_null(self):
+        from colander import null
+        typ = self._makeOne()
+        node = DummySchemaNode()
+        result = typ.serialize(node, null)
+        self.assertEqual(result, null)
+
     def test_serialize_not_a_dict(self):
         typ = self._makeOne()
         node = DummySchemaNode()

File deform/tests/test_widget.py

         from deform.widget import TextInputWidget
         return TextInputWidget(**kw)
 
-    def test_serialize_None_no_default(self):
+    def test_serialize_null(self):
+        from colander import null
+        widget = self._makeOne()
+        renderer = DummyRenderer()
+        field = DummyField(None, renderer=renderer)
+        widget.serialize(field, null)
+        self.assertEqual(renderer.template, widget.template)
+        self.assertEqual(renderer.kw['field'], field)
+        self.assertEqual(renderer.kw['cstruct'], '')
+
+    def test_serialize_None(self):
         widget = self._makeOne()
         renderer = DummyRenderer()
         field = DummyField(None, renderer=renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], '')
 
-    def test_serialize_None_with_default(self):
-        widget = self._makeOne()
-        renderer = DummyRenderer()
-        field = DummyField(None, renderer=renderer)
-        field.default = 'default'
-        widget.serialize(field, None)
-        self.assertEqual(renderer.template, widget.template)
-        self.assertEqual(renderer.kw['field'], field)
-        self.assertEqual(renderer.kw['cstruct'], 'default')
-
-    def test_serialize_not_None(self):
+    def test_serialize_not_null(self):
         widget = self._makeOne()
         renderer = DummyRenderer()
         schema = DummySchema()
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_serialize_not_None_readonly(self):
+    def test_serialize_not_null_readonly(self):
         widget = self._makeOne()
         renderer = DummyRenderer()
         schema = DummySchema()
         result = widget.deserialize(field, pstruct)
         self.assertEqual(result, ' abc ')
 
-    def test_deserialize_None(self):
+    def test_deserialize_null(self):
+        from colander import null
         widget = self._makeOne(strip=False)
         field = DummyField()
-        result = widget.deserialize(field, None)
-        self.assertEqual(result, '')
+        result = widget.deserialize(field, null)
+        self.assertEqual(result, null)
+
+    def test_deserialize_emptystring(self):
+        from colander import null
+        widget = self._makeOne()
+        field = DummyField()
+        pstruct = ''
+        result = widget.deserialize(field, pstruct)
+        self.assertEqual(result, null)
 
 class TestDateInputWidget(unittest.TestCase):
     def _makeOne(self, **kw):
         from deform.widget import DateInputWidget
         return DateInputWidget(**kw)
 
-    def test_serialize_None_no_default(self):
+    def test_serialize_null(self):
+        from colander import null
+        widget = self._makeOne()
+        renderer = DummyRenderer()
+        field = DummyField(None, renderer=renderer)
+        widget.serialize(field, null)
+        self.assertEqual(renderer.template, widget.template)
+        self.assertEqual(renderer.kw['field'], field)
+        self.assertEqual(renderer.kw['cstruct'], '')
+
+    def test_serialize_None(self):
         widget = self._makeOne()
         renderer = DummyRenderer()
         field = DummyField(None, renderer=renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], '')
 
-    def test_serialize_None_with_default(self):
-        widget = self._makeOne()
-        renderer = DummyRenderer()
-        field = DummyField(None, renderer=renderer)
-        field.default = 'default'
-        widget.serialize(field, None)
-        self.assertEqual(renderer.template, widget.template)
-        self.assertEqual(renderer.kw['field'], field)
-        self.assertEqual(renderer.kw['cstruct'], 'default')
-
-    def test_serialize_not_None(self):
+    def test_serialize_not_null(self):
         widget = self._makeOne()
         renderer = DummyRenderer()
         schema = DummySchema()
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_serialize_not_None_readonly(self):
+    def test_serialize_not_null_readonly(self):
         widget = self._makeOne()
         renderer = DummyRenderer()
         schema = DummySchema()
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_deserialize_None(self):
+    def test_deserialize_null(self):
+        from colander import null
         widget = self._makeOne()
         field = DummyField()
-        result = widget.deserialize(field, None)
-        self.assertEqual(result, '')
+        result = widget.deserialize(field, null)
+        self.assertEqual(result, null)
+
+    def test_deserialize_emptystring(self):
+        from colander import null
+        widget = self._makeOne()
+        field = DummyField()
+        result = widget.deserialize(field, '')
+        self.assertEqual(result, null)
 
 class TestHiddenWidget(unittest.TestCase):
     def _makeOne(self, **kw):
         from deform.widget import HiddenWidget
         return HiddenWidget(**kw)
     
-    def test_serialize_None_no_default(self):
+    def test_serialize_null(self):
+        from colander import null
+        widget = self._makeOne()
+        renderer = DummyRenderer()
+        field = DummyField(None, renderer=renderer)
+        widget.serialize(field, null)
+        self.assertEqual(renderer.template, widget.template)
+        self.assertEqual(renderer.kw['field'], field)
+        self.assertEqual(renderer.kw['cstruct'], '')
+
+    def test_serialize_None(self):
         widget = self._makeOne()
         renderer = DummyRenderer()
         field = DummyField(None, renderer=renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], '')
 
-    def test_serialize_None_with_default(self):
-        widget = self._makeOne()
-        renderer = DummyRenderer()
-        field = DummyField(None, renderer=renderer)
-        field.default = 'default'
-        widget.serialize(field, None)
-        self.assertEqual(renderer.template, widget.template)
-        self.assertEqual(renderer.kw['field'], field)
-        self.assertEqual(renderer.kw['cstruct'], 'default')
-
-    def test_serialize_not_None(self):
+    def test_serialize_not_null(self):
         widget = self._makeOne()
         renderer = DummyRenderer()
         schema = DummySchema()
         result = widget.deserialize(field, pstruct)
         self.assertEqual(result, 'abc')
 
-    def test_deserialize_None(self):
+    def test_deserialize_null(self):
+        from colander import null
         widget = self._makeOne(strip=False)
         field = DummyField()
-        result = widget.deserialize(field, None)
-        self.assertEqual(result, '')
+        result = widget.deserialize(field, null)
+        self.assertEqual(result, null)
 
 class TestPasswordWidget(TestTextInputWidget):
     def _makeOne(self, **kw):
         from deform.widget import CheckboxWidget
         return CheckboxWidget(**kw)
 
-    def test_serialize_None(self):
-        renderer = DummyRenderer()
-        schema = DummySchema()
-        field = DummyField(schema, renderer)
-        field.default = 'default'
-        widget = self._makeOne()
-        widget.serialize(field, None)
-        self.assertEqual(renderer.template, widget.template)
-        self.assertEqual(renderer.kw['field'], field)
-        self.assertEqual(renderer.kw['cstruct'], 'default')
-
-    def test_serialize_not_None(self):
+    def test_serialize_not_null(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_serialize_not_None_readonly(self):
+    def test_serialize_not_null_readonly(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_deserialize_None(self):
+    def test_deserialize_null(self):
+        from colander import null
         widget = self._makeOne()
         field = DummyField()
-        result = widget.deserialize(field, None)
+        result = widget.deserialize(field, null)
         self.assertEqual(result, 'false')
 
     def test_deserialize_true_val(self):
         from deform.widget import RadioChoiceWidget
         return RadioChoiceWidget(**kw)
 
-    def test_serialize_None(self):
+    def test_serialize_null(self):
+        from colander import null
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
-        field.default = 'default'
         widget = self._makeOne()
-        widget.serialize(field, None)
+        widget.serialize(field, null)
         self.assertEqual(renderer.template, widget.template)
         self.assertEqual(renderer.kw['field'], field)
-        self.assertEqual(renderer.kw['cstruct'], 'default')
+        self.assertEqual(renderer.kw['cstruct'], '')
 
-    def test_serialize_not_None(self):
+    def test_serialize_null_alternate_null_value(self):
+        from colander import null
+        renderer = DummyRenderer()
+        schema = DummySchema()
+        field = DummyField(schema, renderer)
+        widget = self._makeOne()
+        widget.null_value = 'fred'
+        widget.serialize(field, null)
+        self.assertEqual(renderer.template, widget.template)
+        self.assertEqual(renderer.kw['field'], field)
+        self.assertEqual(renderer.kw['cstruct'], 'fred')
+
+    def test_serialize_not_null(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_serialize_not_None_readonly(self):
+    def test_serialize_not_null_readonly(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_deserialize_None(self):
+    def test_deserialize_null(self):
+        from colander import null
         widget = self._makeOne()
         field = DummyField()
-        result = widget.deserialize(field, None)
-        self.assertEqual(result, '')
+        result = widget.deserialize(field, null)
+        self.assertEqual(result, null)
 
     def test_deserialize_other(self):
         widget = self._makeOne()
         from deform.widget import SelectWidget
         return SelectWidget(**kw)
 
+    def test_serialize_null(self):
+        from colander import null
+        renderer = DummyRenderer()
+        schema = DummySchema()
+        field = DummyField(schema, renderer)
+        widget = self._makeOne()
+        widget.serialize(field, null)
+        self.assertEqual(renderer.template, widget.template)
+        self.assertEqual(renderer.kw['field'], field)
+        self.assertEqual(renderer.kw['cstruct'], '')
+
     def test_serialize_None(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
-        field.default = 'default'
         widget = self._makeOne()
         widget.serialize(field, None)
         self.assertEqual(renderer.template, widget.template)
         self.assertEqual(renderer.kw['field'], field)
-        self.assertEqual(renderer.kw['cstruct'], 'default')
+        self.assertEqual(renderer.kw['cstruct'], '')
 
-    def test_serialize_not_None(self):
+    def test_serialize_null_alternate_null_value(self):
+        from colander import null
+        renderer = DummyRenderer()
+        schema = DummySchema()
+        field = DummyField(schema, renderer)
+        widget = self._makeOne()
+        widget.null_value = 'fred'
+        widget.serialize(field, null)
+        self.assertEqual(renderer.template, widget.template)
+        self.assertEqual(renderer.kw['field'], field)
+        self.assertEqual(renderer.kw['cstruct'], 'fred')
+
+    def test_serialize_not_null(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_serialize_not_None_readonly(self):
+    def test_serialize_not_null_readonly(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_deserialize_None(self):
+    def test_deserialize_null(self):
+        from colander import null
         widget = self._makeOne()
         field = DummyField()
-        result = widget.deserialize(field, None)
-        self.assertEqual(result, '')
+        result = widget.deserialize(field, null)
+        self.assertEqual(result, null)
+
+    def test_deserialize_null_value(self):
+        from colander import null
+        widget = self._makeOne()
+        field = DummyField()
+        result = widget.deserialize(field, '')
+        self.assertEqual(result, null)
 
     def test_deserialize_other(self):
         widget = self._makeOne()
         from deform.widget import CheckboxChoiceWidget
         return CheckboxChoiceWidget(**kw)
 
-    def test_serialize_None(self):
+    def test_serialize_null(self):
+        from colander import null
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
-        field.default = ()
         widget = self._makeOne()
-        widget.serialize(field, None)
+        widget.serialize(field, null)
         self.assertEqual(renderer.template, widget.template)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], ())
 
-    def test_serialize_None_with_default_None(self):
+    def test_serialize_None(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], ())
 
-    def test_serialize_not_None(self):
+    def test_serialize_not_null(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_serialize_not_None_readonly(self):
+    def test_serialize_not_null_readonly(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_deserialize_None(self):
+    def test_deserialize_null(self):
+        from colander import null
         widget = self._makeOne()
         field = DummyField()
-        result = widget.deserialize(field, None)
-        self.assertEqual(result, ())
+        result = widget.deserialize(field, null)
+        self.assertEqual(result, null)
 
     def test_deserialize_single_string(self):
         # If only one checkbox was checked:  DAMN HTTP forms!
         from deform.widget import CheckedInputWidget
         return CheckedInputWidget(**kw)
 
+    def test_serialize_null(self):
+        from colander import null
+        renderer = DummyRenderer()
+        schema = DummySchema()
+        field = DummyField(schema, renderer)
+        widget = self._makeOne()
+        widget.serialize(field, null)
+        self.assertEqual(renderer.template, widget.template)
+        self.assertEqual(renderer.kw['field'], field)
+        self.assertEqual(renderer.kw['cstruct'], '')
+
     def test_serialize_None(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
-        field.default = 'default'
-        widget = self._makeOne()
-        widget.serialize(field, None)
-        self.assertEqual(renderer.template, widget.template)
-        self.assertEqual(renderer.kw['field'], field)
-        self.assertEqual(renderer.kw['cstruct'], 'default')
-
-    def test_serialize_None_no_default(self):
-        renderer = DummyRenderer()
-        schema = DummySchema()
-        field = DummyField(schema, renderer)
-        field.default = None
         widget = self._makeOne()
         widget.serialize(field, None)
         self.assertEqual(renderer.template, widget.template)
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
-        field.default = None
         widget = self._makeOne()
         widget.serialize(field, True)
         self.assertEqual(renderer.template, widget.template)
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
-        field.default = None
         widget = self._makeOne()
         widget.serialize(field, False)
         self.assertEqual(renderer.template, widget.template)
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
-        field.default = None
         widget = self._makeOne()
         widget.serialize(field, True, readonly=True)
         self.assertEqual(renderer.template, widget.readonly_template)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], True)
 
-    def test_deserialize_None(self):
+    def test_deserialize_null(self):
+        from colander import null
         widget = self._makeOne()
         field = DummyField()
-        result = widget.deserialize(field, None)
-        self.assertEqual(result, '')
+        result = widget.deserialize(field, null)
+        self.assertEqual(result, null)
+
+    def test_deserialize_empty(self):
+        from colander import null
+        widget = self._makeOne()
+        field = DummyField()
+        result = widget.deserialize(field, {'value':'',
+                                            'confirm':''})
+        self.assertEqual(result, null)
+        self.assertEqual(field.error, None)
 
     def test_deserialize_nonmatching(self):
         widget = self._makeOne()
         self.assertEqual(result, 'password')
         self.assertEqual(field.error, None)
 
-
 class TestCheckedPasswordWidget(TestCheckedInputWidget):
     def _makeOne(self, **kw):
         from deform.widget import CheckedPasswordWidget
         from deform.widget import FileUploadWidget
         return FileUploadWidget(tmpstore, **kw)
 
-    def test_serialize_None_with_default(self):
+    def test_serialize_null(self):
+        from colander import null
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
-        field.default = {'uid':'abc'}
         tmpstore = DummyTmpStore()
         widget = self._makeOne(tmpstore)
-        widget.serialize(field, None)
+        widget.serialize(field, null)
         self.assertEqual(renderer.template, widget.template)
         self.assertEqual(renderer.kw['field'], field)
-        self.assertEqual(renderer.kw['cstruct'], field.default)
-        self.assertEqual(tmpstore['abc'], field.default)
+        self.assertEqual(renderer.kw['cstruct'], {})
 
-    def test_serialize_None_no_default(self):
+    def test_serialize_None(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
-        field.default = None
         tmpstore = DummyTmpStore()
         widget = self._makeOne(tmpstore)
         widget.serialize(field, None)
         self.assertEqual(renderer.template, widget.readonly_template)
         self.assertEqual(tmpstore['uid'], existing)
 
+    def test_deserialize_null(self):
+        from colander import null
+        schema = DummySchema()
+        field = DummyField(schema)
+        tmpstore = DummyTmpStore()
+        widget = self._makeOne(tmpstore)
+        result = widget.deserialize(field, null)
+        self.assertEqual(result, null)
+
     def test_deserialize_no_file_selected_no_previous_file(self):
+        from colander import null
         schema = DummySchema()
         field = DummyField(schema)
         tmpstore = DummyTmpStore()
         widget = self._makeOne(tmpstore)
         result = widget.deserialize(field, {})
-        self.assertEqual(result, None)
+        self.assertEqual(result, null)
 
     def test_deserialize_no_file_selected_with_previous_file(self):
         schema = DummySchema()
         self.assertEqual(result, 'abc')
 
     def test_deserialize_no_file_selected_with_previous_file_missing(self):
+        from colander import null
         schema = DummySchema()
         field = DummyField(schema)
         tmpstore = DummyTmpStore()
         widget = self._makeOne(tmpstore)
         result = widget.deserialize(field, {'uid':'uid'})
-        self.assertEqual(result, None)
+        self.assertEqual(result, null)
 
     def test_deserialize_file_selected_no_previous_file(self):
         schema = DummySchema()
         from deform.widget import DatePartsWidget
         return DatePartsWidget(**kw)
 
-    def test_serialize_None(self):
-        renderer = DummyRenderer()
-        schema = DummySchema()
-        field = DummyField(schema, renderer)
-        field.default = '2010-12-1'
-        widget = self._makeOne()
-        widget.serialize(field, None)
-        self.assertEqual(renderer.template, widget.template)
-        self.assertEqual(renderer.kw['year'], '2010')
-        self.assertEqual(renderer.kw['month'], '12')
-        self.assertEqual(renderer.kw['day'], '1')
-
-    def test_serialize_None_no_default(self):
+    def test_serialize_null(self):
+        from colander import null
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         widget = self._makeOne()
-        widget.serialize(field, None)
+        widget.serialize(field, null)
         self.assertEqual(renderer.template, widget.template)
         self.assertEqual(renderer.kw['year'], '')
         self.assertEqual(renderer.kw['month'], '')
         self.assertEqual(renderer.kw['day'], '')
 
-    def test_serialize_not_None(self):
+    def test_serialize_not_null(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['month'], '12')
         self.assertEqual(renderer.kw['day'], '1')
 
-    def test_serialize_not_None_readonly(self):
+    def test_serialize_not_null_readonly(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['month'], '12')
         self.assertEqual(renderer.kw['day'], '1')
 
-    def test_deserialize_not_None(self):
+    def test_deserialize_not_null(self):
         schema = DummySchema()
         field = DummyField(schema, None)
         widget = self._makeOne()
                                     {'year':'01', 'month':'2', 'day':'3'})
         self.assertEqual(result, '01-2-3')
 
-    def test_deserialize_None(self):
+    def test_deserialize_null(self):
+        from colander import null
         schema = DummySchema()
         field = DummyField(schema, None)
         widget = self._makeOne()
-        result = widget.deserialize(field, None)
-        self.assertEqual(result, '')
+        result = widget.deserialize(field, null)
+        self.assertEqual(result, null)
 
     def test_deserialize_incomplete(self):
         schema = DummySchema()
                         field, {'year':'1', 'month':'', 'day':''})
         self.assertEqual(e.msg, 'Incomplete')
 
+
 class TestMappingWidget(unittest.TestCase):
     def _makeOne(self, **kw):
         from deform.widget import MappingWidget
         return MappingWidget(**kw)
 
+    def test_serialize_null(self):
+        from colander import null
+        renderer = DummyRenderer()
+        schema = DummySchema()
+        field = DummyField(schema, renderer)
+        widget = self._makeOne()
+        widget.serialize(field, null)
+        self.assertEqual(renderer.template, widget.template)
+        self.assertEqual(renderer.kw['field'], field)
+        self.assertEqual(renderer.kw['cstruct'], {})
+
     def test_serialize_None(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], {})
 
-    def test_serialize_not_None(self):
+    def test_serialize_not_null(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_serialize_not_None_readonly(self):
+    def test_serialize_not_null_readonly(self):
         renderer = DummyRenderer()
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], cstruct)
 
-    def test_deserialize_None(self):
+    def test_deserialize_null(self):
+        from colander import null
         widget = self._makeOne()
         field = DummyField()
-        result = widget.deserialize(field, None)
+        result = widget.deserialize(field, null)
         self.assertEqual(result, {})
 
-    def test_deserialize_non_None(self):
+    def test_deserialize_non_null(self):
         widget = self._makeOne()
         field = DummyField()
         inner_field = DummyField()
         self.assertEqual(urllib.unquote(result), 'abc')
         self.assertEqual(protofield.cloned, True)
 
+    def test_serialize_null(self):
+        from colander import null
+        renderer = DummyRenderer('abc')
+        schema = DummySchema()
+        field = DummyField(schema, renderer)
+        inner = DummyField()
+        field.children=[inner]
+        widget = self._makeOne()
+        result = widget.serialize(field, null)
+        self.assertEqual(result, 'abc')
+        self.assertEqual(len(renderer.kw['subfields']), 0)
+        self.assertEqual(renderer.kw['field'], field)
+        self.assertEqual(renderer.kw['cstruct'], [])
+        self.assertEqual(renderer.template, widget.template)
+
     def test_serialize_None(self):
         renderer = DummyRenderer('abc')
         schema = DummySchema()
         inner = DummyField()
         field.children=[inner]
         widget = self._makeOne()
-        result = widget.serialize(field)
+        result = widget.serialize(field, None)
         self.assertEqual(result, 'abc')
         self.assertEqual(len(renderer.kw['subfields']), 0)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], [])
         self.assertEqual(renderer.template, widget.template)
 
-    def test_serialize_None_render_initial_item(self):
+    def test_serialize_null_render_initial_item(self):
+        from colander import null
         renderer = DummyRenderer('abc')
         schema = DummySchema()
         field = DummyField(schema, renderer)
         field.children=[inner]
         widget = self._makeOne()
         widget.render_initial_item = True
-        result = widget.serialize(field)
+        result = widget.serialize(field, null)
         self.assertEqual(result, 'abc')
         self.assertEqual(len(renderer.kw['subfields']), 1)
         self.assertEqual(renderer.kw['field'], field)
-        self.assertEqual(renderer.kw['cstruct'], [None])
+        self.assertEqual(renderer.kw['cstruct'], [null])
         self.assertEqual(renderer.template, widget.template)
 
     def test_serialize_add_subitem_value(self):
+        from colander import null
         renderer = DummyRenderer('abc')
         schema = DummySchema()
         field = DummyField(schema, renderer)
         field.children=[inner]
         widget = self._makeOne()
         widget.add_subitem_text_template = 'Yo ${subitem_description}'
-        widget.serialize(field)
+        widget.serialize(field, null)
         self.assertEqual(renderer.kw['add_subitem_text'].interpolate(),
                          'Yo description')
 
     def test_serialize_subitem_value(self):
+        from colander import null
         renderer = DummyRenderer('abc')
         schema = DummySchema()
         field = DummyField(schema, renderer)
         inner = DummyField()
         field.children=[inner]
         widget = self._makeOne()
-        widget.serialize(field)
+        widget.serialize(field, null)
         self.assertEqual(renderer.kw['item_field'], inner)
 
-    def test_serialize_not_None(self):
+    def test_serialize_not_null(self):
         renderer = DummyRenderer('abc')
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['cstruct'], ['123'])
         self.assertEqual(renderer.template, widget.template)
 
-    def test_serialize_not_None_readonly(self):
+    def test_serialize_not_null_readonly(self):
         renderer = DummyRenderer('abc')
         schema = DummySchema()
         field = DummyField(schema, renderer)
         self.assertEqual(renderer.kw['cstruct'], ['123'])
         self.assertEqual(renderer.template, widget.template)
 
-    def test_deserialize_None(self):
+    def test_deserialize_null(self):
+        from colander import null
         field = DummyField()
         inner_field = DummyField()
         field.children = [inner_field]
         widget = self._makeOne()
-        result = widget.deserialize(field, None)
+        result = widget.deserialize(field, null)
         self.assertEqual(result, [])
         self.assertEqual(field.sequence_fields, [])
 
-    def test_deserialize_not_None(self):
+    def test_deserialize_not_null(self):
         field = DummyField()
         inner_field = DummyField()
         inner_field.widget = DummyWidget()
         from deform.widget import TextAreaCSVWidget
         return TextAreaCSVWidget(**kw)
 
-    def test_serialize_None_no_default(self):
+    def test_serialize_null(self):
+        from colander import null
         widget = self._makeOne()
         renderer = DummyRenderer()
         field = DummyField(None, renderer=renderer)
-        widget.serialize(field, None)
+        widget.serialize(field, null)
         self.assertEqual(renderer.template, widget.template)
         self.assertEqual(renderer.kw['field'], field)
         self.assertEqual(renderer.kw['cstruct'], '')
 
-    def test_serialize_None_with_default(self):
-        widget = self._makeOne()
-        renderer = DummyRenderer()
-        field = DummyField(None, renderer=renderer)
-        field.default = [('a', 1)]
-        widget.serialize(field, None)
-        self.assertEqual(renderer.template, widget.template)
-        self.assertEqual(renderer.kw['field'], field)
-        self.assertEqual(renderer.kw['cstruct'], 'a,1\r\n')
-
     def test_serialize_with_unparseable(self):
         widget = self._makeOne()
         renderer = DummyRenderer()
         self.assertRaises(colander.Invalid, widget.deserialize, field, pstruct)
         self.assertEqual(field.unparseable, pstruct)
 
-    def test_deserialize_None_not_required(self):
+    def test_deserialize_null(self):
+        from colander import null
         widget = self._makeOne(strip=False)
         schema = DummySchema()
         schema.required = False
         field = DummyField(schema=schema)
-        result = widget.deserialize(field, None)
-        self.assertEqual(result, [])
+        result = widget.deserialize(field, null)
+        self.assertEqual(result, null)
 
-    def test_deserialize_None_required(self):
-        import colander
+    def test_deserialize_emptystring(self):
+        from colander import null
         widget = self._makeOne(strip=False)
         schema = DummySchema()
-        schema.required = True
+        schema.required = False
         field = DummyField(schema=schema)
-        self.assertRaises(colander.Invalid, widget.deserialize, field, None)
+        result = widget.deserialize(field, '')
+        self.assertEqual(result, null)
 
     def test_handle_error_outermost_has_msg(self):
         widget = self._makeOne()
         self.cloned = True
         return self
 
+    def deserialize(self, pstruct):
+        return self.widget.deserialize(self, pstruct)
+
 class DummyTmpStore(dict):
     def preview_url(self, uid):
         return 'preview_url'

File deform/widget.py

+import colander
 import csv
 import random
 import string
 import urllib
 
 from colander import Invalid
+from colander import null
+
 from deform.i18n import _
 
 class Widget(object):
     def __init__(self, **kw):
         self.__dict__.update(kw)
 
-    def serialize(self, field, cstruct=None, readonly=False):
+    def serialize(self, field, cstruct, readonly=False):
         """
         The ``serialize`` method of a widget must serialize a
         :term:`cstruct` value to an HTML rendering.  A :term:`cstruct`
         """
         raise NotImplementedError
 
-    def deserialize(self, field, pstruct=None):
+    def deserialize(self, field, pstruct):
         """
         The ``deserialize`` method of a widget must deserialize a
         :term:`pstruct` value to a :term:`cstruct` value and return the
         raise NotImplementedError
 
     def handle_error(self, field, error):
+        """
+        The ``handle_error`` method of a widget must:
+
+        - Set the ``error`` attribute of the ``field`` object it is
+          passed, if the ``error`` attribute has not already been set.
+
+        - Call the ``handle_error`` method of each subfield which also
+          has an error (as per the ``error`` argument's ``children``
+          attribute).
+        """
         if field.error is None:
             field.error = error
         # XXX exponential time
                     subfield.widget.handle_error(subfield, e)
 
 
+class DateInputWidget(Widget):
+    """
+    
+    Renders an ``<input type="date"/>`` date picker widget (uses JQuery Tools
+    to paint the control if the browser is not HTML5-aware).  Most useful when
+    the schema  node is a ``colander.Date`` object.
+
+    **Attributes/Arguments**
+
+    size
+        The size, in columns, of the text input field.  Defaults to
+        ``None``, meaning that the ``size`` is not included in the
+        widget output (uses browser default size).
+
+    template
+        The template name used to render the widget.  Default:
+        ``dateinput``.
+
+    readonly_template
+        The template name used to render the widget in read-only mode.
+        Default: ``readonly/textinput``.
+    """
+    template = 'dateinput'
+    readonly_template = 'readonly/textinput'
+    size = None
+
+    def serialize(self, field, cstruct=colander.null, readonly=False):
+        if cstruct is colander.null:
+            cstruct = ''
+        template = readonly and self.readonly_template or self.template
+        return field.renderer(template, field=field, cstruct=cstruct)
+
+    def deserialize(self, field, pstruct):
+        if pstruct is colander.null:
+            return colander.null
+        return pstruct
+
 class TextInputWidget(Widget):
     """
     Renders an ``<input type="text"/>`` widget.
     mask = None
     mask_placeholder = "_"
 
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
-            cstruct = field.default
-        if cstruct is None:
+    def serialize(self, field, cstruct, readonly=False):
+        if cstruct in (null, None):
             cstruct = ''
         template = readonly and self.readonly_template or self.template
         return field.renderer(template, field=field, cstruct=cstruct)
 
     def deserialize(self, field, pstruct):
-        if pstruct is None:
-            pstruct = ''
+        if pstruct is null:
+            return null
         if self.strip:
             pstruct = pstruct.strip()
+        if not pstruct:
+            return null
         return pstruct
 
 class DateInputWidget(Widget):
     readonly_template = 'readonly/textinput'
     size = None
 
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
-            cstruct = field.default
-        if cstruct is None:
+    def serialize(self, field, cstruct, readonly=False):
+        if cstruct in (null, None):
             cstruct = ''
         template = readonly and self.readonly_template or self.template
         return field.renderer(template, field=field, cstruct=cstruct)
 
     def deserialize(self, field, pstruct):
-        if pstruct is None:
-            pstruct = ''
+        if pstruct in ('', null):
+            return null
         return pstruct
 
 class TextAreaWidget(TextInputWidget):
     template = 'hidden'
     hidden = True
 
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
-            cstruct = field.default
-        if cstruct is None:
+    def serialize(self, field, cstruct, readonly=False):
+        if cstruct in (null, None):
             cstruct = ''
         return field.renderer(self.template, field=field, cstruct=cstruct)
 
     def deserialize(self, field, pstruct):
-        if pstruct is None:
-            pstruct = ''
+        if pstruct is null:
+            return null
         return pstruct
 
 class CheckboxWidget(Widget):
     template = 'checkbox'
     readonly_template = 'readonly/checkbox'
 
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
-            cstruct = field.default
+    def serialize(self, field, cstruct, readonly=False):
         template = readonly and self.readonly_template or self.template
         return field.renderer(template, field=field, cstruct=cstruct)
 
     def deserialize(self, field, pstruct):
-        if pstruct is None:
-            pstruct = self.false_val
+        if pstruct is null:
+            return self.false_val
         return (pstruct == self.true_val) and self.true_val or self.false_val
 
 class SelectWidget(Widget):
         returned when the form is posted.  The second is the display
         value.
 
+    null_value
+        The value which represents the null value.  When the null
+        value is encountered during serialization, the
+        :attr:`colander.null` sentinel is returned to the caller.
+        Default: ``''`` (the empty string).
+
     template
         The template name used to render the widget.  Default:
         ``select``.
     readonly_template
         The template name used to render the widget in read-only mode.
         Default: ``readonly/select``.
+
     """
     template = 'select'
     readonly_template = 'readonly/select'
+    null_value = ''
     values = ()
 
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
-            cstruct = field.default
+    def serialize(self, field, cstruct, readonly=False):
+        if cstruct in (null, None):
+            cstruct = self.null_value
         template = readonly and self.readonly_template or self.template
         return field.renderer(template, field=field, cstruct=cstruct)
 
     def deserialize(self, field, pstruct):
-        if pstruct is None:
-            pstruct = ''
+        if pstruct in (null, self.null_value):
+            return null
         return pstruct
 
 class RadioChoiceWidget(SelectWidget):
     readonly_template
         The template name used to render the widget in read-only mode.
         Default: ``readonly/radio_choice``.
+
+    null_value
+        The value used to replace the ``colander.null`` value when it
+        is passed to the ``serialize`` or ``deserialize`` method.
+        Default: the empty string.
     """
     template = 'radio_choice'
     readonly_template = 'readonly/radio_choice'
     readonly_template
         The template name used to render the widget in read-only mode.
         Default: ``readonly/checkbox_choice``.
+
+    null_value
+        The value used to replace the ``colander.null`` value when it
+        is passed to the ``serialize`` or ``deserialize`` method.
+        Default: the empty string.
     """
     template = 'checkbox_choice'
     readonly_template = 'readonly/checkbox_choice'
     values = ()
 
-
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
-            cstruct = field.default
-        if cstruct is None:
+    def serialize(self, field, cstruct, readonly=False):
+        if cstruct in (null, None):
             cstruct = ()
         template = readonly and self.readonly_template or self.template
         return field.renderer(template, field=field, cstruct=cstruct)
 
     def deserialize(self, field, pstruct):
-        if pstruct is None:
-            pstruct = ()
+        if pstruct is null:
+            return null
         if isinstance(pstruct, basestring):
             return (pstruct,)
         return tuple(pstruct)
     mask = None
     mask_placeholder = "_"
 
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
-            cstruct = field.default
-        if cstruct is None:
+    def serialize(self, field, cstruct, readonly=False):
+        if cstruct in (null, None):
             cstruct = ''
         confirm = getattr(field, 'confirm', '')
         template = readonly and self.readonly_template or self.template
                               )
 
     def deserialize(self, field, pstruct):
-        if pstruct is None:
-            pstruct = {}
+        if pstruct is null:
+            return null
         value = pstruct.get('value') or ''
         confirm = pstruct.get('confirm') or ''
         field.confirm = confirm
-        if value != confirm:
+        if (value or confirm) and (value != confirm):
             raise Invalid(field.schema, self.mismatch_message, value)
+        if not value:
+            return null
         return value
 
 class CheckedPasswordWidget(CheckedInputWidget):
     error_class = None
     category = 'structural'
 
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
+    def serialize(self, field, cstruct, readonly=False):
+        if cstruct in (null, None):
             cstruct = {}
         template = readonly and self.readonly_template or self.template
-        return field.renderer(template, field=field, cstruct=cstruct)
+        return field.renderer(template, field=field, cstruct=cstruct,
+                              null=null)
 
     def deserialize(self, field, pstruct):
         error = None
+        
         result = {}
 
-        if pstruct is None:
+        if pstruct is null:
             pstruct = {}
 
         for num, subfield in enumerate(field.children):
             name = subfield.name
-            subval = pstruct.get(name)
+            subval = pstruct.get(name, null)
             try:
-                result[name] = subfield.widget.deserialize(subfield, subval)
+                result[name] = subfield.deserialize(subval)
             except Invalid, e:
                 result[name] = e.value
                 if error is None:
         # automated testing; finding last node)
         item_field = field.children[0].clone()
         proto = field.renderer(self.item_template, field=item_field,
-                               cstruct=None, parent=field)
+                               cstruct=null, parent=field)
         if isinstance(proto, unicode):
             proto = proto.encode('utf-8')
         proto = urllib.quote(proto)
         return proto
 
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
+    def serialize(self, field, cstruct, readonly=False):
+        if cstruct in (null, None):
             if self.render_initial_item:
-                cstruct = [None]
+                cstruct = [null]
             else:
                 cstruct = []
 
         result = []
         error = None
 
-        if pstruct is None:
+        if pstruct is null:
             pstruct = []
 
         field.sequence_fields = []
         for num, substruct in enumerate(pstruct):
             subfield = item_field.clone()
             try:
-                subval = subfield.widget.deserialize(subfield, substruct)
+                subval = subfield.deserialize(substruct)
             except Invalid, e:
                 subval = e.value
                 if error is None:
         return ''.join(
             [random.choice(string.uppercase+string.digits) for i in range(10)])
 
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
-            cstruct = field.default
-        if cstruct is None:
+    def serialize(self, field, cstruct, readonly=False):
+        if cstruct in (null, None):
             cstruct = {}
         if cstruct:
             uid = cstruct['uid']
         return field.renderer(template, field=field, cstruct=cstruct)
 
     def deserialize(self, field, pstruct):
+        if pstruct is null:
+            return null
+
         upload = pstruct.get('upload')
         uid = pstruct.get('uid')
 
             # the upload control had no file selected
             if uid is None:
                 # no previous file exists
-                return None
+                return null
             else:
                 # a previous file should exist
                 data = self.tmpstore.get(uid)
                 # but if it doesn't, don't blow up
                 if data is None:
-                    return None
+                    return null
 
         return data
 
     size = None
     assume_y2k = True
 
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
-            cstruct = field.default
-        if cstruct is None:
+    def serialize(self, field, cstruct, readonly=False):
+        if cstruct is null:
             year = ''
             month = ''
             day = ''
                               year=year, month=month, day=day)
 
     def deserialize(self, field, pstruct):
-        if pstruct is None:
-            return ''
+        if pstruct is null:
+            return null
         else:
             if self.assume_y2k:
                 year = pstruct['year']
     cols = None
     rows = None
 
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
-            cstruct = field.default
-        if cstruct is None:
+    def serialize(self, field, cstruct, readonly=False):
+        if cstruct is null:
             cstruct = []
         textrows = getattr(field, 'unparseable', None)
         if textrows is None:
         return field.renderer(template, field=field, cstruct=textrows)
         
     def deserialize(self, field, pstruct):
-        if pstruct is None:
-            pstruct = ''
-        if not pstruct.strip() and field.schema.required:
-            # prevent
-            raise Invalid(field.schema, 'Required', [])
+        if pstruct is null:
+            return null
+        if not pstruct.strip():
+            return null
         try:
             infile = StringIO.StringIO(pstruct)
             reader = csv.reader(infile)

File deformdemo/app.py

         self.request = request
         self.macros = get_template('templates/main.pt').macros
 
-    def render_form(self, form, appstruct=None, submitted='submit',
+    def render_form(self, form, appstruct=colander.null, submitted='submit',
                     success=None, readonly=False):
 
         captured = None
         form = deform.Form(schema, buttons=('submit',))
         return self.render_form(form)
 
+    @bfg_view(renderer='templates/form.pt', name='nonrequiredfields')
+    @demonstrate('Non-Required Fields')
+    def nonrequiredfields(self):
+        class Schema(colander.Schema):
+            required = colander.SchemaNode(
+                colander.String(),
+                description='Required Field'
+                )
+            notrequired = colander.SchemaNode(
+                colander.String(),
+                missing=u'',
+                description='Unrequired Field')
+        schema = Schema()
+        form = deform.Form(schema, buttons=('submit',))
+        return self.render_form(form)
+
     @bfg_view(renderer='templates/form.pt', name='unicodeeverywhere')
     @demonstrate('Unicode Everywhere')
     def unicodeeverywhere(self):
         class Schema(colander.Schema):
             one = colander.SchemaNode(
                 colander.String(),
-                default='',
+                missing=u'',
                 title='One (required if Two is not supplied)')
             two = colander.SchemaNode(
                 colander.String(),
-                default='',
+                missing=u'',
                 title='Two (required if One is not supplied)')
         def validator(form, value):
             if not value['one'] and not value['two']:
     def __getattr__(self, name):
         return getattr(self.widget, name)
 
-    def serialize(self, field, cstruct=None, readonly=False):
-        if cstruct is None:
-            cstruct = field.default
-        if cstruct is None:
+    def serialize(self, field, cstruct, readonly=False):
+        if cstruct is colander.null:
             cstruct = []
         textrows = getattr(field, 'unparseable', None)
         if textrows is None:
 
     def deserialize(self, field, pstruct):
         text = self.widget.deserialize(field, pstruct)
-        if not text.strip() and field.schema.required:
-            # prevent
-            raise colander.Invalid(field.schema, 'Required', [])
+        if text is colander.null:
+            return text
+        if not text.strip():
+            return colander.null
         try:
             infile = StringIO.StringIO(text)
             reader = csv.reader(infile)

File deformdemo/tests/test_demo.py

     def test_render_default(self):
         browser.open(self.url)
         self.failIf(browser.is_element_present('css=.errorMsgLbl'))
-        self.failIf(browser.is_element_present('css=#req-deformField1'))
-        self.failIf(browser.is_element_present('css=#req-deformField2'))
+        self.failUnless(browser.is_element_present('css=#req-deformField1'))
+        self.failUnless(browser.is_element_present('css=#req-deformField2'))
         self.failUnless(browser.is_element_present('css=#req-deformField3'))
         self.assertEqual(browser.get_value('deformField1'), 'Grandaddy')
         self.assertEqual(browser.get_attribute('deformField1@name'), 'artist')
             browser.get_text('css=#captured'),
             u"{'album': u'def', 'song': u'ghi', 'artist': u'abc'}")
 
+class NonRequiredFieldTests(unittest.TestCase):
+    url = "/nonrequiredfields/"
+    def test_render_default(self):
+        browser.open(self.url)
+        self.failIf(browser.is_element_present('css=.errorMsgLbl'))
+        self.failUnless(browser.is_element_present('css=#req-deformField1'))
+        self.assertEqual(browser.get_value('deformField1'), '')
+        self.assertEqual(browser.get_attribute('deformField1@name'), 'required')
+        self.assertEqual(browser.get_value('deformField2'), '')
+        self.assertEqual(browser.get_attribute('deformField2@name'),
+                         'notrequired')
+        self.assertEqual(browser.get_text('css=#captured'), 'None')
+
+    def test_submit_empty(self):
+        browser.open(self.url)
+        browser.wait_for_page_to_load("30000")
+        browser.click('submit')
+        browser.wait_for_page_to_load("30000")
+        self.failUnless(browser.is_element_present('css=.errorMsgLbl'))
+        self.assertEqual(browser.get_value('deformField1'), '')
+        self.assertEqual(browser.get_value('deformField2'), '')
+        self.assertEqual(browser.get_text('css=#error-deformField1'),
+                         'Required')
+        self.assertEqual(browser.get_text('css=#captured'), 'None')
+
+    def test_submit_success_required_filled_notrequired_empty(self):
+        browser.open(self.url)
+        browser.wait_for_page_to_load("30000")
+        browser.type('deformField1', 'abc')
+        browser.click('submit')
+        browser.wait_for_page_to_load("30000")
+        self.failIf(browser.is_element_present('css=.errorMsgLbl'))
+        self.assertEqual(browser.get_value('deformField1'), 'abc')
+        self.assertEqual(browser.get_value('deformField2'), '')
+        self.assertEqual(
+            browser.get_text('css=#captured'),
+            u"{'required': u'abc', 'notrequired': u''}")
+
+    def test_submit_success_required_and_notrequired_filled(self):
+        browser.open(self.url)
+        browser.wait_for_page_to_load("30000")
+        browser.type('deformField1', 'abc')
+        browser.type('deformField2', 'def')
+        browser.click('submit')
+        browser.wait_for_page_to_load("30000")
+        self.failIf(browser.is_element_present('css=.errorMsgLbl'))
+        self.assertEqual(browser.get_value('deformField1'), 'abc')
+        self.assertEqual(browser.get_value('deformField2'), 'def')
+        self.assertEqual(
+            browser.get_text('css=#captured'),
+            u"{'required': u'abc', 'notrequired': u'def'}")
+
 class HiddenFieldWidgetTests(unittest.TestCase):
     url = "/hidden_field/"
     def test_render_default(self):
         self.assertEqual(browser.get_text('css=#captured'), 'None')
 
     def test_submit_default(self):
+        from decimal import Decimal
         browser.open(self.url)
         browser.wait_for_page_to_load("30000")
         browser.click('submit')
                          '1,hello,4.5\n2,goodbye,5.5')
         captured = browser.get_text('css=#captured')
         self.assertEqual(
-            captured,
-            (u'{\'csv\': [(1, u\'hello\', Decimal("4.5")), '
-            u'(2, u\'goodbye\', Decimal("5.5"))]}'))
+            eval(captured),
+            ({'csv': [(1, u'hello', Decimal("4.5")), 
+                      (2, u'goodbye', Decimal("5.5"))]
+              }))
 
     def test_submit_line_error(self):
         browser.open(self.url)
         browser.click('submit')
         browser.wait_for_page_to_load("30000")
         captured = browser.get_text('css=#captured')
-        self.assertEqual(captured, u"{'two': '', 'one': u'one'}")
+        self.assertEqual(captured, u"{'two': u'', 'one': u'one'}")
         self.failIf(browser.is_element_present('css=.errorMsgLbl'))
 
 class AjaxFormTests(unittest.TestCase):

File docs/Makefile

 # You can set these variables from the command line.
 SPHINXOPTS    =
 SPHINXBUILD   = sphinx-build
+SPHINXBUILD   = /home/chrism/projects/repoze/svn/deform/trunk/env/bin/sphinx-build
 PAPER         =
 
 # Internal variables.

File docs/basics.rst

    <http://docs.repoze.org/colander>`_
 
 A schema is composed of one or more *schema node* objects, each
-typically of the class :class:`colander.SchemaNode`, usually in a nested
-arrangement.  Each schema node object has a required *type*, an
-optional *validator*, an optional *default*, an optional *title*, an
-optional *description*, and a slightly less optional *name*.
+typically of the class :class:`colander.SchemaNode`, usually in a
+nested arrangement.  Each schema node object has a required *type*, an
+optional *validator*, an optional *default*, an optional *missing*, an
+optional *title*, an optional *description*, and a slightly less
+optional *name*.
 
 The *type* of a schema node indicates its data type (such as
 :class:`colander.Int` or :class:`colander.String`).
 ``validator=colander.Range(0, 200)``.  A validator is not called after
 schema node serialization, only after node deserialization.
 
-The *default* of a schema node indicates its default value if a value
-for the schema node is not found in the input data during
-serialization.  It should be the *deserialized* representation.  If a
-schema node does not have a default, it is considered a required
-schema node.
+The *default* of a schema node indicates the value to be serialized if
+a value for the schema node is not found in the input data during
+serialization.  It should be the deserialized representation.  If a
+schema node does not have a default, it is considered "serialization
+required".
+
+The *missing* of a schema node indicates the value to be deserialized
+if a value for the schema node is not found in the input data during
+deserialization.  It should be the deserialized representation.  If a
+schema node does not have a default, it is considered "deserialization
+required".
 
 The *name* of a schema node is used to relate schema nodes to each
 other.  It is also used as the title if a title is not provided.

File docs/serialization.rst

 .. code-block:: python
    :linenos:
 
-    def deserialize(self, field, pstruct):
-        if pstruct is None:
-            pstruct = {}
-        value = pstruct.get('value') or ''
-        confirm = pstruct.get('confirm') or ''
-        field.confirm = confirm
-        if value != confirm:
-            raise Invalid(field.schema, self.mismatch_message, value)
-        return value
+    import colander