simplify defaulting

This commit is contained in:
Chris McDonough
2010-06-09 14:22:40 +00:00
parent 90f98b6e67
commit f8dee4f914
11 changed files with 555 additions and 768 deletions

View File

@@ -17,24 +17,68 @@ Next release
- Raise a ``TypeError`` when bogus keyword arguments are passed to - Raise a ``TypeError`` when bogus keyword arguments are passed to
``colander.SchemaNode``. ``colander.SchemaNode``.
- Upgrade explanations required: ``partial`` argument and attribute of - ``missing`` constructor arg to SchemaNode: signifies
colander.MappingSchema has been removed, ``null`` added to *deserialization* default, disambiguated from ``default`` which acted
serialization data structure for partials instead of omitting them as both serialization and deserialization default previously.
from output like before, ``missing`` constructor arg to SchemaNode,
nulls may be present in serialized and deserialized data structures, Changes necessitated / made possible by SchemaNode ``missing``
``sdefault`` attribute of SchemaNode has been removed, ``srequired`` addition:
attribute of SchemaNode has been added, the ``value`` argument to
``deserialize`` is now named ``cstruct``, the ``value`` argument`` - The ``allow_empty`` argument of the ``colander.String`` type was
to ``serialize`` is now named ``appstruct``, types must now expect removed (use ``missing=''`` as a wrapper SchemaNode argument
``colander.default`` instead of ``None`` during ``serialize`` as instead).
``appstruct``, types must now expect ``colander.default`` instead of
``None`` during ``deserialize`` as ``cstruct``, - New concept: ``colander.null`` input to serialization and
``colander.SchemaNode.serialize`` and deserialization. Use of ``colander.null` normalizes serialization
``colander.SchemaNode.deserialize`` now require no explicit value and deserialization default handling.
argument (value defaults to ``colander.default``), ``allow_empty``
argument of ``colander.String`` type removed (use ``missing=''`` Changes necessitated / made possible by ``colander.null`` addition:
instead in the surrounding schemanode), serialization and
deserialization of ``null`` in ``colander.String`` returns null now. - ``partial`` argument and attribute of colander.MappingSchema has
been removed; all serializations are partial, and partial
deserializations are not necessary.
- ``colander.null`` values are added to the cstruct for partial
serializations instead of omitting missing node values from
the cstruct.
- ``colander.null`` may now be present in serialized and
deserialized data structures.
- ``sdefault`` attribute of SchemaNode has been removed; we never need
to serialize a default anymore.
- The value ``colander.null`` will be passed as ``appstruct`` to
each type's ``serialize`` method when a mapping appstruct doesn't
have a corresponding key instead of ``None``, as was the practice
previously.
- The value ``colander.null`` will be passed as ``cstruct`` to
each type's ``deserialize`` method when a mapping cstruct
doesn't have a corresponding key instead of ``None``, as was the
practice previously.
- Types now must handle ``colander.null`` explicitly during
serialization.
- Updated and expanded documentation, particularly with respect to new
``colander.null`` handling.
- The ``value`` argument`` to the ``serialize`` method of a SchemaNode
is now named ``appstruct``. It is no longer a required argument; it
defaults to ``colander.null`` now.
The ``value`` argument to the ``deserialize`` method of a SchemaNode
is now named ``cstruct``. It is no longer a required argument; it
defaults to ``colander.null`` now.
- The ``value`` argument to the ``serialize`` method of each built-in
type is now named ``appstruct``, and is now required: it is no
longer a keyword argument that has a default.
The ``value`` argument to the ``deserialize`` method of each
built-in type is now named ``cstruct``, and is now required: it is
no longer a keyword argument that has a default.
0.6.2 (2010-05-08) 0.6.2 (2010-05-08)
------------------ ------------------

View File

@@ -8,18 +8,6 @@ import translationstring
_ = translationstring.TranslationStringFactory('colander') _ = translationstring.TranslationStringFactory('colander')
class _marker(object):
def __repr__(self):
return '<MISSING>'
_marker = _marker()
class default(object):
def __repr__(self):
return '<colander.default>'
default = default()
class null(object): class null(object):
def __nonzero__(self): def __nonzero__(self):
return False return False
@@ -361,16 +349,18 @@ class Mapping(object):
Special behavior is exhibited when a subvalue of a mapping is Special behavior is exhibited when a subvalue of a mapping is
present in the schema but is missing from the mapping passed to present in the schema but is missing from the mapping passed to
either the ``serialize`` or ``deserialize`` method of this class. either the ``serialize`` or ``deserialize`` method of this class.
In this case, the :attr:`colander.default` value will be passed to In this case, the :attr:`colander.null` value will be passed to
the schema node representing the subvalue of the mapping. During the ``serialize`` or ``deserialize`` method of the schema node
representing the subvalue of the mapping respectively. During
serialization, this will result in the behavior described in serialization, this will result in the behavior described in
:ref:`serializing_default` for the subnode. During :ref:`serializing_null` for the subnode. During deserialization,
deserialization, this will result in the behavior described in this will result in the behavior described in
:ref:`deserializing_default` for the subnode. :ref:`deserializing_null` for the subnode.
If the :attr:`colander.null` value is passed to the serialize or If the :attr:`colander.null` value is passed to the serialize
deserialize methods of this class, the empty dictionary will be method of this class, a dictionary will be returned, where each of
returned. the values in the returned dictionary is the serialized
representation of the null value for its type.
""" """
def __init__(self, unknown='ignore'): def __init__(self, unknown='ignore'):
self.unknown = unknown self.unknown = unknown
@@ -404,7 +394,7 @@ class Mapping(object):
for num, subnode in enumerate(node.children): for num, subnode in enumerate(node.children):
name = subnode.name name = subnode.name
subval = value.pop(name, default) subval = value.pop(name, null)
try: try:
result[name] = callback(subnode, subval) result[name] = callback(subnode, subval)
except Invalid, e: except Invalid, e:
@@ -438,8 +428,6 @@ class Mapping(object):
return self._impl(node, appstruct, callback) return self._impl(node, appstruct, callback)
def deserialize(self, node, cstruct): def deserialize(self, node, cstruct):
if cstruct is null:
return null
def callback(subnode, subcstruct): def callback(subnode, subcstruct):
return subnode.deserialize(subcstruct) return subnode.deserialize(subcstruct)
@@ -465,9 +453,9 @@ class Tuple(Positional):
when converted to a tuple, have the same number of elements as the when converted to a tuple, have the same number of elements as the
number of the associated node's subnodes. number of the associated node's subnodes.
If the :attr:`colander.null` value is passed to the serialize or If the :attr:`colander.null` value is passed to the serialize
deserialize methods of this class, the :attr:`colander.null` value method of this class, the :attr:`colander.null` value will be
will be returned. returned.
""" """
def _validate(self, node, value): def _validate(self, node, value):
if not hasattr(value, '__iter__'): if not hasattr(value, '__iter__'):
@@ -517,9 +505,6 @@ class Tuple(Positional):
return self._impl(node, appstruct, callback) return self._impl(node, appstruct, callback)
def deserialize(self, node, cstruct): def deserialize(self, node, cstruct):
if cstruct is null:
return null
def callback(subnode, subval): def callback(subnode, subval):
return subnode.deserialize(subval) return subnode.deserialize(subval)
@@ -550,9 +535,8 @@ class Sequence(Positional):
The default value of ``accept_scalar`` is ``False``. The default value of ``accept_scalar`` is ``False``.
If the :attr:`colander.null` value is passed to the serialize or If the :attr:`colander.null` value is passed to the serialize
deserialize methods of this class, the :attr:`colander.null` value method of this class, the :attr:`colander.null` value is returned.
is returned.
""" """
def __init__(self, accept_scalar=False): def __init__(self, accept_scalar=False):
self.accept_scalar = accept_scalar self.accept_scalar = accept_scalar
@@ -618,7 +602,6 @@ class Sequence(Positional):
return self._impl(node, appstruct, callback, accept_scalar) return self._impl(node, appstruct, callback, accept_scalar)
def deserialize(self, node, cstruct, accept_scalar=None): def deserialize(self, node, cstruct, accept_scalar=None):
""" """
Along with the normal ``node`` and ``cstruct`` arguments, this Along with the normal ``node`` and ``cstruct`` arguments, this
method accepts an additional optional keyword argument: method accepts an additional optional keyword argument:
@@ -638,9 +621,6 @@ class Sequence(Positional):
respect the default ``accept_scalar`` value attached to this respect the default ``accept_scalar`` value attached to this
instance via its constructor. instance via its constructor.
""" """
if cstruct is null:
return null
def callback(subnode, subcstruct): def callback(subnode, subcstruct):
return subnode.deserialize(subcstruct) return subnode.deserialize(subcstruct)
@@ -733,9 +713,6 @@ class String(object):
mapping={'val':appstruct, 'err':e}) mapping={'val':appstruct, 'err':e})
) )
def deserialize(self, node, cstruct): def deserialize(self, node, cstruct):
if cstruct is null:
return null
try: try:
result = cstruct result = cstruct
if not isinstance(result, unicode): if not isinstance(result, unicode):
@@ -770,10 +747,7 @@ class Number(object):
mapping={'val':appstruct}), mapping={'val':appstruct}),
) )
def deserialize(self, node, cstruct): def deserialize(self, node, cstruct):
if cstruct is null: if not cstruct:
return null
if cstruct == '':
raise Invalid(node, _('Required')) raise Invalid(node, _('Required'))
try: try:
@@ -788,9 +762,9 @@ class Number(object):
class Integer(Number): class Integer(Number):
""" A type representing an integer. """ A type representing an integer.
If the :attr:`colander.null` value is passed to the serialize or If the :attr:`colander.null` value is passed to the serialize
deserialize methods of this class, the :attr:`colander.null` value method of this class, the :attr:`colander.null` value will be
will be returned. returned.
The subnodes of the :class:`colander.SchemaNode` that wraps The subnodes of the :class:`colander.SchemaNode` that wraps
this type are ignored. this type are ignored.
@@ -802,9 +776,9 @@ Int = Integer
class Float(Number): class Float(Number):
""" A type representing a float. """ A type representing a float.
If the :attr:`colander.null` value is passed to the serialize or If the :attr:`colander.null` value is passed to the serialize
deserialize methods of this class, the :attr:`colander.null` value method of this class, the :attr:`colander.null` value will be
will be returned. returned.
The subnodes of the :class:`colander.SchemaNode` that wraps The subnodes of the :class:`colander.SchemaNode` that wraps
this type are ignored. this type are ignored.
@@ -815,9 +789,9 @@ class Decimal(Number):
""" A type representing a decimal floating point. Deserialization """ A type representing a decimal floating point. Deserialization
returns an instance of the Python ``decimal.Decimal`` type. returns an instance of the Python ``decimal.Decimal`` type.
If the :attr:`colander.null` value is passed to the serialize or If the :attr:`colander.null` value is passed to the serialize
deserialize methods of this class, the :attr:`colander.null` value method of this class, the :attr:`colander.null` value will be
will be returned. returned.
The subnodes of the :class:`colander.SchemaNode` that wraps The subnodes of the :class:`colander.SchemaNode` that wraps
this type are ignored. this type are ignored.
@@ -835,9 +809,9 @@ class Boolean(object):
Serialization will produce ``true`` or ``false`` based on the Serialization will produce ``true`` or ``false`` based on the
value. value.
If the :attr:`colander.null` value is passed to the serialize or If the :attr:`colander.null` value is passed to the serialize
deserialize methods of this class, the :attr:`colander.null` value method of this class, the :attr:`colander.null` value will be
will be returned. returned.
The subnodes of the :class:`colander.SchemaNode` that wraps The subnodes of the :class:`colander.SchemaNode` that wraps
this type are ignored. this type are ignored.
@@ -850,9 +824,6 @@ class Boolean(object):
return appstruct and 'true' or 'false' return appstruct and 'true' or 'false'
def deserialize(self, node, cstruct): def deserialize(self, node, cstruct):
if cstruct is null:
return null
try: try:
result = str(cstruct) result = str(cstruct)
except: except:
@@ -900,9 +871,9 @@ class GlobalObject(object):
was supplied to the constructor, an :exc:`colander.Invalid` error was supplied to the constructor, an :exc:`colander.Invalid` error
will be raised. will be raised.
If the :attr:`colander.null` value is passed to the serialize or If the :attr:`colander.null` value is passed to the serialize
deserialize methods of this class, the :attr:`colander.null` value method of this class, the :attr:`colander.null` value will be
will be returned. returned.
The subnodes of the :class:`colander.SchemaNode` that wraps The subnodes of the :class:`colander.SchemaNode` that wraps
this type are ignored. this type are ignored.
@@ -978,9 +949,6 @@ class GlobalObject(object):
mapping={'val':appstruct}) mapping={'val':appstruct})
) )
def deserialize(self, node, cstruct): def deserialize(self, node, cstruct):
if cstruct is null:
return null
if not isinstance(cstruct, basestring): if not isinstance(cstruct, basestring):
raise Invalid(node, raise Invalid(node,
_('"${val}" is not a string', _('"${val}" is not a string',
@@ -1033,9 +1001,9 @@ class DateTime(object):
does so by using midnight of the day as the time, and uses the does so by using midnight of the day as the time, and uses the
``default_tzinfo`` to give the serialization a timezone. ``default_tzinfo`` to give the serialization a timezone.
If the :attr:`colander.null` value is passed to the serialize or If the :attr:`colander.null` value is passed to the serialize
deserialize methods of this class, the :attr:`colander.null` value method of this class, the :attr:`colander.null` value will be
will be returned. returned.
The subnodes of the :class:`colander.SchemaNode` that wraps The subnodes of the :class:`colander.SchemaNode` that wraps
this type are ignored. this type are ignored.
@@ -1065,9 +1033,6 @@ class DateTime(object):
return appstruct.isoformat() return appstruct.isoformat()
def deserialize(self, node, cstruct): def deserialize(self, node, cstruct):
if cstruct is null:
return null
try: try:
result = iso8601.parse_date(cstruct) result = iso8601.parse_date(cstruct)
except (iso8601.ParseError, TypeError), e: except (iso8601.ParseError, TypeError), e:
@@ -1112,9 +1077,9 @@ class Date(object):
time information related to the serialized value during time information related to the serialized value during
deserialization. deserialization.
If the :attr:`colander.null` value is passed to the serialize or If the :attr:`colander.null` value is passed to the serialize
deserialize methods of this class, the :attr:`colander.null` value method of this class, the :attr:`colander.null` value will be
will be returned. returned.
The subnodes of the :class:`colander.SchemaNode` that wraps The subnodes of the :class:`colander.SchemaNode` that wraps
this type are ignored. this type are ignored.
@@ -1138,9 +1103,6 @@ class Date(object):
return appstruct.isoformat() return appstruct.isoformat()
def deserialize(self, node, cstruct): def deserialize(self, node, cstruct):
if cstruct is null:
return null
try: try:
result = iso8601.parse_date(cstruct) result = iso8601.parse_date(cstruct)
result = result.date() result = result.date()
@@ -1169,19 +1131,18 @@ class SchemaNode(object):
node are not known at construction time, they can later be added node are not known at construction time, they can later be added
via the ``add`` method. via the ``add`` method.
The constructor accepts these keyword arguments (via **kw): The constructor accepts these keyword arguments:
- ``name``: The name of this node. - ``name``: The name of this node.
- ``default``: The default serialization value for this node. - ``default``: The default serialization value for this node.
Default: N/A (optional). If it is not provided, this node has Default: :attr:`colander.null`.
no default value and it will be considered 'serialization
required' (the ``srequired`` attribute will be ``True``).
- ``missing``: The default deserialization value for this node. - ``missing``: The default deserialization value for this node.
If it is not provided, this node has no missing value and it If it is not provided, the missing value of this node will be
will be considered 'required' (the ``required`` attribute will :attr:`colander.null`, indicating that it is considered
be ``True``). 'required' (the ``required`` computed attribute will be
``True``).
- ``validator``: Optional validator for this node. It should be - ``validator``: Optional validator for this node. It should be
an object that implements the an object that implements the
@@ -1192,7 +1153,7 @@ class SchemaNode(object):
by Colander itself). by Colander itself).
- ``description``: The description for this node. Defaults to - ``description``: The description for this node. Defaults to
``''`` (the emtpty string). The description is used by ``''`` (the empty string). The description is used by
higher-level systems (not by Colander itself). higher-level systems (not by Colander itself).
""" """
@@ -1207,8 +1168,8 @@ class SchemaNode(object):
def __init__(self, typ, *children, **kw): def __init__(self, typ, *children, **kw):
self.typ = typ self.typ = typ
self.validator = kw.pop('validator', None) self.validator = kw.pop('validator', None)
self.default = kw.pop('default', _marker) self.default = kw.pop('default', null)
self.missing = kw.pop('missing', _marker) self.missing = kw.pop('missing', null)
self.name = kw.pop('name', '') self.name = kw.pop('name', '')
self.title = kw.pop('title', self.name.capitalize()) self.title = kw.pop('title', self.name.capitalize())
self.description = kw.pop('description', '') self.description = kw.pop('description', '')
@@ -1224,85 +1185,62 @@ class SchemaNode(object):
self.name, self.name,
) )
@property
def srequired(self):
""" A property which returns ``True`` if a usable value
corresponding to this node is required to be present in a data
structure we're asked to serialize. A return value of
``True`` implies that a usable ``default`` value wasn't
specified for this node. A return value of ``False`` implies
that a usable ``default`` value *was* specified for this node."""
return self.default is _marker
@property @property
def required(self): def required(self):
""" A property which returns ``True`` if a usable value """ A property which returns ``True`` if the ``missing`` value
corresponding to this node is required to be present in a data related to this node is the :attr:`colander.null` sentinel
structure we're asked to deserialize. A return value of value.
``True`` implies that a usable ``missing`` value wasn't
specified for this node. A return value of ``False`` implies
that a usable ``missing`` value *was* specified for this node."""
return self.missing is _marker
def serialize(self, appstruct=default): A return value of ``True`` implies that a ``missing`` value
wasn't specified for this node. A return value of ``False``
implies that a ``missing`` value was specified for this node."""
return self.missing is null
def serialize(self, appstruct=null):
""" Serialize the :term:`appstruct` to a :term:`cstruct` based """ Serialize the :term:`appstruct` to a :term:`cstruct` based
on the schema represented by this node and return the on the schema represented by this node and return the
cstruct. cstruct.
If ``appstruct`` is :attr:`colander.default`, do something If ``appstruct`` is :attr:`colander.null`, return the
special: serialized value of this node's ``default`` attribute (by
default, the serialization of :attr:`colander.null`).
- If the ``default`` attribute of this node has been set, return
the serialized value of the ``default`` attribute.
- If the ``default`` attribute of this node has not been set,
return the serialization of :attr:`colander.null`.
If an ``appstruct`` argument is not explicitly provided, it If an ``appstruct`` argument is not explicitly provided, it
defaults to :attr:`colander.default`. defaults to :attr:`colander.null`.
""" """
if appstruct is default: if appstruct is null:
appstruct = self.default appstruct = self.default
if appstruct is _marker:
# We cannot just return null here; we need to allow
# the node to serialize null to what it believes null
# should mean
appstruct = null
cstruct = self.typ.serialize(self, appstruct) cstruct = self.typ.serialize(self, appstruct)
return cstruct return cstruct
def deserialize(self, cstruct=default): def deserialize(self, cstruct=null):
""" Deserialize and validate the :term:`cstruct` into an """ Deserialize and validate the :term:`cstruct` into an
:term:`appstruct` based on the schema, and return the :term:`appstruct` based on the schema, and return the
deserialized, validated appstruct. If the cstruct cannot be deserialized, validated appstruct. If the cstruct cannot be
validated, a :exc:`colander.Invalid` exception will be raised. validated, a :exc:`colander.Invalid` exception will be raised.
If ``cstruct`` is :attr:`colander.default`, do something special: If ``cstruct`` is :attr:`colander.null`, do something special:
- If the ``missing`` attribute of this node has been set, - If the ``missing`` attribute of this node has been set
return it. explicitly, return its value. No deserialization or
validation of this value is performed; it is simply
returned.
- If the ``missing`` attribute of this node has not been set, - If the ``missing`` attribute of this node has not been set
raise a :exc:`colander.Invalid` exception error. explicitly, raise a :exc:`colander.Invalid` exception error.
If a ``cstruct`` argument is not explicitly provided, it If a ``cstruct`` argument is not explicitly provided, it
defaults to :attr:`colander.default`. defaults to :attr:`colander.null`.
When used as a cstruct, :attr:`colander.null` is never passed to
a validator: it is considered intrinsically valid.
""" """
if cstruct is default: if cstruct is null:
appstruct = self.missing appstruct = self.missing
if appstruct is _marker: if appstruct is null:
raise Invalid(self, _('Required')) raise Invalid(self, _('Required'))
# We never validate the missing value # We never deserialize or validate the missing value
return appstruct return appstruct
appstruct = self.typ.deserialize(self, cstruct) appstruct = self.typ.deserialize(self, cstruct)
if appstruct is not null:
# We never validate the null value.
if self.validator is not None: if self.validator is not None:
self.validator(self, appstruct) self.validator(self, appstruct)
return appstruct return appstruct

View File

@@ -1,54 +1,47 @@
def Validator(struct, value): def Validator(node, value):
""" """
A validator is called after deserialization of a value. A validator is called after deserialization of a value.
If ``value`` is not valid, raise a :class:`colander.Invalid` If ``value`` is not valid, raise a :class:`colander.Invalid`
instance as an exception after. instance as an exception after.
``struct`` is a :class:`colander.Structure` instance which ``node`` is a :class:`colander.SchemaNode` instance, for use when
contains, among other things, the default value, the name of the raising a :class:`colander.Invalid` exception.
value, and a ``required`` flag indicating whether this value is
required. It is often ignored in simple validators.
""" """
class Type(object): class Type(object):
def serialize(self, struct, value): def serialize(self, node, appstruct):
""" """
Serialize the object represented by ``value`` to a Serialize the :term:`appstruct` represented by ``appstruct``
data structure. The serialization should be composed of one or to a :term:`cstruct`. The serialization should be composed of
more objects which can be deserialized by the one or more objects which can be deserialized by the
:meth:`colander.interfaces.Type.deserialize` method of this :meth:`colander.interfaces.Type.deserialize` method of this
type. type.
This method should also do type validation of ``value``. ``node`` is a :class:`colander.SchemaNode` instance.
``struct`` is a :class:`colander.Structure` instance which ``appstruct`` is an :term:`appstruct`.
contains, among other things, the default value, the name of
the value, and a ``required`` flag indicating whether this
value is required.
If the object cannot be serialized, or type validation for If ``appstruct`` is the special value :attr:`colander.null`,
``value`` fails, a :exc:`colander.Invalid` exception should be the type should serialize a null value.
raised.
If the object cannot be serialized for any reason, a
:exc:`colander.Invalid` exception should be raised.
""" """
def deserialize(self, struct, value): def deserialize(self, node, cstruct):
""" """
Deserialze the serialization represented by ``value`` to a Deserialze the :term:`cstruct` represented by ``cstruct`` to
data structure. The deserialization should be composed of one an :term:`appstruct`. The deserialization should be composed
or more objects which can be serialized by the of one or more objects which can be serialized by the
:meth:`colander.interfaces.Type.serialize` method of this :meth:`colander.interfaces.Type.serialize` method of this
type. type.
This method should also do type validation of ``value``. ``node`` is a :class:`colander.SchemaNode` instance.
``struct`` is a :class:`colander.Structure` instance which ``cstruct`` is a :term:`cstruct`.
contains, among other things, the default value, the name of
the value, and a ``required`` flag indicating whether this
value is required.
If the object cannot be deserialized, or type validation for If the object cannot be deserialized for any reason, a
``value`` fails, a :exc:`colander.Invalid` exception should be :exc:`colander.Invalid` exception should be raised.
raised.
""" """

View File

@@ -326,13 +326,6 @@ class TestMapping(unittest.TestCase):
except ValueError, e: # pragma: no cover except ValueError, e: # pragma: no cover
raise AssertionError(e) raise AssertionError(e)
def test_deserialize_null(self):
from colander import null
node = DummySchemaNode(None)
typ = self._makeOne()
result = typ.deserialize(node, null)
self.assertEqual(result, null)
def test_deserialize_not_a_mapping(self): def test_deserialize_not_a_mapping(self):
node = DummySchemaNode(None) node = DummySchemaNode(None)
typ = self._makeOne() typ = self._makeOne()
@@ -353,14 +346,6 @@ class TestMapping(unittest.TestCase):
result = typ.deserialize(node, {'a':1}) result = typ.deserialize(node, {'a':1})
self.assertEqual(result, {'a':1}) self.assertEqual(result, {'a':1})
def test_deserialize_value_is_null(self):
node = DummySchemaNode(None)
from colander import null
node.children = [DummySchemaNode(None, name='a')]
typ = self._makeOne()
result = typ.deserialize(node, null)
self.assertEqual(result, null)
def test_deserialize_unknown_raise(self): def test_deserialize_unknown_raise(self):
node = DummySchemaNode(None) node = DummySchemaNode(None)
node.children = [DummySchemaNode(None, name='a')] node.children = [DummySchemaNode(None, name='a')]
@@ -397,7 +382,7 @@ class TestMapping(unittest.TestCase):
] ]
typ = self._makeOne() typ = self._makeOne()
result = typ.deserialize(node, {'a':1}) result = typ.deserialize(node, {'a':1})
self.assertEqual(result, {'a':1, 'b':colander.default}) self.assertEqual(result, {'a':1, 'b':colander.null})
def test_serialize_null(self): def test_serialize_null(self):
import colander import colander
@@ -439,24 +424,16 @@ class TestMapping(unittest.TestCase):
def test_serialize_value_is_null(self): def test_serialize_value_is_null(self):
node = DummySchemaNode(None) node = DummySchemaNode(None)
from colander import null from colander import null
from colander import default
node.children = [DummySchemaNode(None, name='a')] node.children = [DummySchemaNode(None, name='a')]
typ = self._makeOne() typ = self._makeOne()
result = typ.serialize(node, null) result = typ.serialize(node, null)
self.assertEqual(result, {'a':default}) self.assertEqual(result, {'a':null})
class TestTuple(unittest.TestCase): class TestTuple(unittest.TestCase):
def _makeOne(self): def _makeOne(self):
from colander import Tuple from colander import Tuple
return Tuple() return Tuple()
def test_deserialize_null(self):
import colander
node = DummySchemaNode(None)
typ = self._makeOne()
result = typ.deserialize(node, colander.null)
self.assertEqual(result, colander.null)
def test_deserialize_not_iterable(self): def test_deserialize_not_iterable(self):
node = DummySchemaNode(None) node = DummySchemaNode(None)
typ = self._makeOne() typ = self._makeOne()
@@ -573,13 +550,6 @@ class TestSequence(unittest.TestCase):
from colander import Sequence from colander import Sequence
self.assertEqual(Seq, Sequence) self.assertEqual(Seq, Sequence)
def test_deserialize_null(self):
import colander
node = DummySchemaNode(None)
typ = self._makeOne()
result = typ.deserialize(node, colander.null)
self.assertEqual(result, colander.null)
def test_deserialize_not_iterable(self): def test_deserialize_not_iterable(self):
node = DummySchemaNode(None) node = DummySchemaNode(None)
typ = self._makeOne() typ = self._makeOne()
@@ -682,13 +652,6 @@ class TestString(unittest.TestCase):
result = typ.deserialize(node, '') result = typ.deserialize(node, '')
self.assertEqual(result, '') self.assertEqual(result, '')
def test_deserialize_null(self):
from colander import null
node = DummySchemaNode(None)
typ = self._makeOne(None)
result = typ.deserialize(node, null)
self.assertEqual(result, null)
def test_deserialize_uncooperative(self): def test_deserialize_uncooperative(self):
val = Uncooperative() val = Uncooperative()
node = DummySchemaNode(None) node = DummySchemaNode(None)
@@ -816,14 +779,6 @@ class TestInteger(unittest.TestCase):
result = typ.deserialize(node, val) result = typ.deserialize(node, val)
self.assertEqual(result, 1) self.assertEqual(result, 1)
def test_deserialize_null(self):
import colander
val = colander.null
node = DummySchemaNode(None)
typ = self._makeOne()
result = typ.deserialize(node, val)
self.assertEqual(result, colander.null)
def test_serialize_fails(self): def test_serialize_fails(self):
val = 'P' val = 'P'
node = DummySchemaNode(None) node = DummySchemaNode(None)
@@ -872,14 +827,6 @@ class TestFloat(unittest.TestCase):
result = typ.deserialize(node, val) result = typ.deserialize(node, val)
self.assertEqual(result, 1.0) self.assertEqual(result, 1.0)
def test_deserialize_null(self):
import colander
val = colander.null
node = DummySchemaNode(None)
typ = self._makeOne()
result = typ.deserialize(node, val)
self.assertEqual(result, colander.null)
def test_serialize_fails(self): def test_serialize_fails(self):
val = 'P' val = 'P'
node = DummySchemaNode(None) node = DummySchemaNode(None)
@@ -929,14 +876,6 @@ class TestDecimal(unittest.TestCase):
result = typ.deserialize(node, val) result = typ.deserialize(node, val)
self.assertEqual(result, decimal.Decimal('1.0')) self.assertEqual(result, decimal.Decimal('1.0'))
def test_deserialize_null(self):
import colander
val = colander.null
node = DummySchemaNode(None)
typ = self._makeOne()
result = typ.deserialize(node, val)
self.assertEqual(result, colander.null)
def test_serialize_fails(self): def test_serialize_fails(self):
val = 'P' val = 'P'
node = DummySchemaNode(None) node = DummySchemaNode(None)
@@ -984,14 +923,6 @@ class TestBoolean(unittest.TestCase):
e = invalid_exc(typ.deserialize, node, Uncooperative()) e = invalid_exc(typ.deserialize, node, Uncooperative())
self.failUnless(e.msg.endswith('not a string')) self.failUnless(e.msg.endswith('not a string'))
def test_deserialize_null(self):
import colander
val = colander.null
node = DummySchemaNode(None)
typ = self._makeOne()
result = typ.deserialize(node, val)
self.assertEqual(result, colander.null)
def test_serialize(self): def test_serialize(self):
typ = self._makeOne() typ = self._makeOne()
node = DummySchemaNode(None) node = DummySchemaNode(None)
@@ -1115,14 +1046,6 @@ class TestGlobalObject(unittest.TestCase):
self.assertRaises(ImportError, typ._pkg_resources_style, None, self.assertRaises(ImportError, typ._pkg_resources_style, None,
':notexisting') ':notexisting')
def test_deserialize_null(self):
import colander
val = colander.null
node = DummySchemaNode(None)
typ = self._makeOne()
result = typ.deserialize(node, val)
self.assertEqual(result, colander.null)
def test_deserialize_not_a_string(self): def test_deserialize_not_a_string(self):
typ = self._makeOne() typ = self._makeOne()
node = DummySchemaNode(None) node = DummySchemaNode(None)
@@ -1237,14 +1160,6 @@ class TestDateTime(unittest.TestCase):
expected = dt.isoformat() expected = dt.isoformat()
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_deserialize_null(self):
import colander
val = colander.null
node = DummySchemaNode(None)
typ = self._makeOne()
result = typ.deserialize(node, val)
self.assertEqual(result, colander.null)
def test_deserialize_date(self): def test_deserialize_date(self):
import datetime import datetime
import iso8601 import iso8601
@@ -1318,14 +1233,6 @@ class TestDate(unittest.TestCase):
expected = dt.date().isoformat() expected = dt.date().isoformat()
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_deserialize_null(self):
import colander
val = colander.null
node = DummySchemaNode(None)
typ = self._makeOne()
result = typ.deserialize(node, val)
self.assertEqual(result, colander.null)
def test_deserialize_invalid_ParseError(self): def test_deserialize_invalid_ParseError(self):
node = DummySchemaNode(None) node = DummySchemaNode(None)
typ = self._makeOne() typ = self._makeOne()
@@ -1400,10 +1307,6 @@ class TestSchemaNode(unittest.TestCase):
node = self._makeOne(None, missing=1) node = self._makeOne(None, missing=1)
self.assertEqual(node.required, False) self.assertEqual(node.required, False)
def test_srequired_false(self):
node = self._makeOne(None, default=1)
self.assertEqual(node.srequired, False)
def test_deserialize_no_validator(self): def test_deserialize_no_validator(self):
typ = DummyType() typ = DummyType()
node = self._makeOne(typ) node = self._makeOne(typ)
@@ -1417,35 +1320,19 @@ class TestSchemaNode(unittest.TestCase):
e = invalid_exc(node.deserialize, 1) e = invalid_exc(node.deserialize, 1)
self.assertEqual(e.msg, 'Wrong') self.assertEqual(e.msg, 'Wrong')
def test_deserialize_value_is_default_no_missing(self): def test_deserialize_value_is_null_no_missing(self):
from colander import null
from colander import Invalid
typ = DummyType() typ = DummyType()
node = self._makeOne(typ) node = self._makeOne(typ)
from colander import default self.assertRaises(Invalid, node.deserialize, null)
from colander import Invalid
self.assertRaises(Invalid, node.deserialize, default)
def test_deserialize_value_is_default_with_missing(self): def test_deserialize_value_is_null_with_missing(self):
from colander import null
typ = DummyType() typ = DummyType()
node = self._makeOne(typ) node = self._makeOne(typ)
node.missing = 'abc' node.missing = 'abc'
from colander import default self.assertEqual(node.deserialize(null), 'abc')
self.assertEqual(node.deserialize(default), 'abc')
def test_deserialize_value_is_default_with_missing_null(self):
from colander import null
from colander import default
typ = DummyType()
node = self._makeOne(typ)
node.missing = null
self.assertEqual(node.deserialize(default), null)
def test_deserialize_value_is_null_validator_not_used(self):
from colander import null
typ = DummyType()
validator = DummyValidator(msg='Wrong')
node = self._makeOne(typ, validator=validator)
value = node.deserialize(null)
self.assertEqual(value, null)
def test_deserialize_noargs_uses_default(self): def test_deserialize_noargs_uses_default(self):
typ = DummyType() typ = DummyType()
@@ -1459,20 +1346,19 @@ class TestSchemaNode(unittest.TestCase):
result = node.serialize(1) result = node.serialize(1)
self.assertEqual(result, 1) self.assertEqual(result, 1)
def test_serialize_value_is_default_no_default(self): def test_serialize_value_is_null_no_default(self):
from colander import null
typ = DummyType() typ = DummyType()
node = self._makeOne(typ) node = self._makeOne(typ)
from colander import default result = node.serialize(null)
from colander import null
result = node.serialize(default)
self.assertEqual(result, null) self.assertEqual(result, null)
def test_serialize_value_is_default_with_default(self): def test_serialize_value_is_null_with_default(self):
from colander import null
typ = DummyType() typ = DummyType()
node = self._makeOne(typ) node = self._makeOne(typ)
node.default = 1 node.default = 1
from colander import default result = node.serialize(null)
result = node.serialize(default)
self.assertEqual(result, 1) self.assertEqual(result, 1)
def test_serialize_noargs_uses_default(self): def test_serialize_noargs_uses_default(self):
@@ -1725,12 +1611,6 @@ class TestDeclarative(unittest.TestCase, TestFunctional):
schema = MainSchema() schema = MainSchema()
return schema return schema
class Test_default(unittest.TestCase):
def test___repr__(self):
from colander import default
self.assertEqual(repr(default), '<colander.default>')
class Test_null(unittest.TestCase): class Test_null(unittest.TestCase):
def test___nonzero__(self): def test___nonzero__(self):
from colander import null from colander import null
@@ -1740,11 +1620,6 @@ class Test_null(unittest.TestCase):
from colander import null from colander import null
self.assertEqual(repr(null), '<colander.null>') self.assertEqual(repr(null), '<colander.null>')
class Test__marker(unittest.TestCase):
def test___repr__(self):
from colander import _marker
self.assertEqual(repr(_marker), '<MISSING>')
class Dummy(object): class Dummy(object):
pass pass

View File

@@ -112,4 +112,5 @@ Schema-Related
.. autoclass:: SequenceSchema .. autoclass:: SequenceSchema
.. attribute:: null

View File

@@ -109,11 +109,11 @@ serialization. It should be the deserialized representation. If a
schema node does not have a default, it is considered "serialization schema node does not have a default, it is considered "serialization
required". required".
The *missing* of a schema node indicates the value to be deserialized The *missing* of a schema node indicates the value if a value for the
if a value for the schema node is not found in the input data during schema node is not found in the input data during deserialization. It
deserialization. It should be the deserialized representation. If a should be the deserialized representation. If a schema node does not
schema node does not have a default, it is considered "deserialization have a default, it is considered "deserialization required". This
required". value is never validated; it is considered pre-validated.
The *name* of a schema node appears in error reports. The *name* of a schema node appears in error reports.
@@ -413,13 +413,13 @@ The value for ``serialized`` above will be ``{'age':'20',
Serialization and deserialization are not completely symmetric, Serialization and deserialization are not completely symmetric,
however. Although schema-driven data conversion happens during however. Although schema-driven data conversion happens during
serialization, and defaults are injected as necessary, :mod:`colander` serialization, and default values are injected as necessary,
types are defined in such a way that structural validation and :mod:`colander` types are defined in such a way that structural
validation of values does *not* happen as it does during validation and validation of values does *not* happen as it does
deserialization. For example, the :attr:`colander.null` value is during deserialization. For example, the :attr:`colander.null` value
substituted for every missing subvalue in an appstruct, and none of is substituted into the cstruct for every missing subvalue in an
the validators associated with the schema or any of is nodes is appstruct, and none of the validators associated with the schema or
invoked. any of is nodes is invoked.
This usually means you may "partially" serialize an appstruct where This usually means you may "partially" serialize an appstruct where
some of the values are missing. If we try to serialize partial data some of the values are missing. If we try to serialize partial data
@@ -438,7 +438,7 @@ string, and the missing ``name`` attribute has been replaced with
:attr:`colander.null`. Above, even though we did not include the :attr:`colander.null`. Above, even though we did not include the
``name`` attribute in the appstruct we fed to ``serialize``, an error ``name`` attribute in the appstruct we fed to ``serialize``, an error
is *not* raised. For more information about :attr:`colander.null` is *not* raised. For more information about :attr:`colander.null`
substitution during serialization, see :ref:`serializing_default`. substitution during serialization, see :ref:`serializing_null`.
The corollary: it is the responsibility of the developer to ensure he The corollary: it is the responsibility of the developer to ensure he
serializes "the right" data; :mod:`colander` will not raise an error serializes "the right" data; :mod:`colander` will not raise an error

View File

@@ -1,405 +0,0 @@
.. _default_and_null:
Default and Null Values
=======================
Two sentinel values have special meanings during serialization and
deserialization: :attr:`colander.default` and :attr:`colander.null`.
Both :attr:`colander.default` and :attr:`colander.null` are used as
sentinel values during the serialization and deserialization
processes, but they are not equivalent. Each represents a different
concept.
:attr:`colander.default` is a sentinel value which may be passed to
:meth:`colander.SchemaNode.serialize` or to
:meth:`colander.SchemaNode.deserialize`. The use of
:attr:`colander.default` indicates that the value corresponding to the
node it's passed to is missing, and if possible, the *default value*
(during serialization, see :ref:`serializing_default`) or *missing
value* (during deserialization, see :ref:`deserializing_default`) for
the corresponding node should be used instead.
.. note::
It makes sense for :attr:`colander.default` to be present in a data
structure passed to :meth:`colander.SchemaNode.serialize` or to
:meth:`colander.SchemaNode.deserialize` but it should never be
present in a schema definition and it should never be present in
the output of a serialization or deserialization. For example, it
is not reasonable to use the :attr:`colander.default` value itself
as the ``default`` or ``missing`` argument to a
:class:`colander.SchemaNode` constructor. Passing
:attr:`colander.default` as the ``default`` or ``missing``
arguments to a schema node constructor will not do anything useful
(it is not explicitly prevented, it's just nonsensical).
:attr:`colander.default` should also never be present in the result
of serialization or the result of deserialization: it will only
ever be present in the input, never in the output.
:attr:`colander.null` is a sentinel representing that the *null* value
for a corresponding node should be serialized (see
:ref:`serializing_null`) or deserialized (see
:ref:`deserializing_null`). The :attr:`colander.null` value may be
present directly in the data structure passed to
:meth:`colander.SchemaNode.serialize` or
:meth:`colander.SchemaNdoe.deserialize` but it is also not uncommon
for :attr:`colander.null` to be the *default value* (``default``) or
*missing value* (``missing``) for a node.
.. note::
Unlike :attr:`colander.default`, :attr:`colander.null` is useful
both within the data structure passed to
:meth:`colander.SchemaNode.serialize` and
:meth:`colander.SchemaNode.deserialize` and within a schema
definition.
.. _serializing_default_and_null:
Serializing Default and Null Values
-----------------------------------
It is possible to serialize both the default and null values.
.. _serializing_default:
Serializing The :attr:`colander.default` Value
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A node will attempt to serialize its *default value* during
:meth:`colander.SchemaNode.serialize` if a value it is provided is
*unspecified*. *Unspecified* means:
#) The value expected by the schema is present in the data structure
passed to :meth:`colander.SchemaNode.serialize` but it is the
literal value :attr:`colander.default`.
#) The value expected by the schema is a subkey of a mapping, but that
key is missing from the mapping in the data structure passed to
:meth:`colander.SchemaNode.serialize`:
The *default value* of a node is specified during schema creation as
its ``default`` attribute / argument. For example, the ``hair_color``
node below has a default value of ``brown``:
.. code-block:: python
import colander
class Person(colander.MappingSchema):
name = colander.SchemaNode(colander.String())
age = colander.SchemaNode(colander.Int(),
validator=colander.Range(0, 200))
hair_color = colander.SchemaNode(colander.String(), default='brown')
Because the ``hair_color`` node is passed a ``default`` value, if the
above schema is used to serialize a mapping that does not have a
``hair_color`` key, the default will be serialized:
.. code-block:: python
schema = Person()
serialized = schema.serialize({'name':'Fred', 'age':20})
Even though we did not include the ``hair_color`` attribute in the
data we fed to ``serialize``, the value of ``serialized`` above will
be ``{'name':'Fred, 'age':'20', 'hair_color':'brown'}``. This is due
to the ``default`` value provided during schema node construction for
``hair_color``.
The same outcome would have been true had we fed the schema a mapping
for serialization which had the :attr:`colander.default` sentinel as
the ``hair_color`` value:
.. code-block:: python
from colander import default
schema = Person()
serialized = schema.serialize({'name':'Fred', 'age':20,
'hair_color':default})
In the above, the value of ``serialized`` above will be
``{'name':'Fred, 'age':'20', 'hair_color':'brown'}`` just as it was in
the example where ``hair_color`` was not present in the mapping.
On the other hand, if the ``hair_color`` value is missing or
:attr:`colander.default`, and the schema does *not* name a ``default``
value for ``hair_color``, it will be present in the resulting
serialization as :attr:`colander.null`:
.. code-block:: python
import colander
class Person(colander.MappingSchema):
name = colander.SchemaNode(colander.String())
age = colander.SchemaNode(colander.Int(),
validator=colander.Range(0, 200))
hair_color = colander.SchemaNode(colander.String())
schema = Person()
serialized = schema.serialize({'name':'Fred', 'age':20})
The value for ``serialized`` above will be ``{'name':'Fred,
'age':'20', 'hair_color':colander.null}``. We did not include the
``hair_color`` attribute in the data we fed to ``serialize``, and
there was no ``default`` value associated with ``hair_color`` to fall
back to, so the :attr:`colander.null` value is used in the resulting
serialization.
Serializations can be done of partial data structures; the
:attr:`colander.null` value is inserted into the serialization
whenever a corresponding value in the data structure being serialized
is missing.
.. note:: The injection of the :attr:`colander.null` value into a
serialization when a default doesn't exist for the corresponding
node is not a behavior shared during both serialization and
deserialization. While a *serialization* can be performed against
a partial data structure without corresponding node defaults, a
*deserialization* cannot be done to partial data without
corresponding node ``missing`` values. When a value is missing
from a data structure being deserialized, and no ``missing`` value
exists for the node corresponding to the missing item in the data
structure, a :class:`colander.Invalid` exception will be the
result.
.. _serializing_null:
Serializing The :attr:`colander.null` Value
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The value :attr:`colander.null` has special meaning to types during
serialization. If :attr:`colander.null` is used as the serialization
value passed to a type, it signals that the type should serialize a
type-specific *null value*.
Serialization of a *null value* is completely type-specific, meaning
each type is free to serialize :attr:`colander.null` to a value that
makes sense for that particular type. For example, the null
serialization value of a :class:`colander.String` type is the empty
string.
The :attr:`colander.null` value will be passed to a type either
directly or indirectly:
- directly: because :attr:`colander.null` is passed directly to the
``serialize`` method of a node.
- indirectly: because a node uses a :attr:`colander.null` value as its
``default`` attribute and the value passed to the serialize method
of a node is missing or :attr:`colander.default` (see
:ref:`serializing_default_and_null`).
When a particular type cannot serialize the null value to anything
sensible, the type's serialize method must return the null object
itself as a serialization. For example, when the
:class:`colander.Boolean` type is asked to serialize the
:attr:`colander.null` value, its ``serialize`` method simply returns
the :attr:`colander.null` value (because null is conceptually neither
true nor false). Therefore, when :attr:`colander.null` is used as
input to serialization, or as the default value of a schema node, it
is possible that the :attr:`colander.null` value will placed into the
serialized data structure. The consumer of the serialization must
anticipate this and deal with the special :attr:`colander.null` value
in the output however it sees fit.
Here's an example of a serialization which will have the sentinel
value :attr:`colander.null` in the serialized output:
.. code-block:: python
import colander
class Person(colander.MappingSchema):
name = colander.SchemaNode(colander.String())
age = colander.SchemaNode(colander.Int(), default=colander.null)
Because the ``age`` node is passed a ``default`` value of
:attr:`colander.null`, if the above schema is used to serialize a
mapping that does not have an ``age`` key, the default will be
serialized into the output:
.. code-block:: python
schema = Person()
serialized = schema.serialize({'name':'Fred'})
The value for ``serialized`` above will be ``{'name':'Fred,
'age':colander.null}``. We did not include the ``age`` attribute in
the data we fed to ``serialize``, but there was a ``default`` value
associated with ``age`` to fall back to: :attr:`colander.null`.
However, the :class:`colander.Int` type cannot serialize null to any
*particular* integer, so it returns the :attr:`colander.null` object
itself. As a result, the raw :attr:`colander.null` value is simply
injected into the resulting serialization. The caller of the
:meth:`colander.SchemaNode.serialize` method will need to deal with
this value appropriately.
Serialization Combinations
~~~~~~~~~~~~~~~~~~~~~~~~~~
To reduce the potential for confusion about the difference between
:attr:`colander.default` and :attr:`colander.null` during
serialization, here's a table of serialization combinations. Within
this table, the ``Value`` column represents the value passed to the
:meth:`colander.SchemaNode.serialize` method of a particular schema
node, the ``Default`` column represents the ``default`` value of that
schema node, and the ``Result`` column is a description of the result
of invoking the :meth:`colander.SchemaNode.serialize` method of the
schema node with the effective value.
===================== ===================== ===========================
Value Default Result
===================== ===================== ===========================
colander.default <missing> Invalid exception raised
<missing> <missing> Invalid exception raised
colander.default value value serialized
<missing> value value serialized
colander.default colander.null null serialized
<missing> colander.null null serialized
value <missing> value serialized
value_a value_b value_a serialized
value colander.null value serialized
colander.null <missing> null serialized
colander.null value null serialized
colander.null colander.null null serialized
===================== ===================== ===========================
.. note:: ``<missing>`` in the above table represents the circumstance
in which a key present in a :class:`colander.MappingSchema` is not
present in a mapping passed to its
:meth:`colander.SchemaNode.serialize` method. In reality,
``<missing>`` means exactly the same thing as
:attr:`colander.default`, because the :class:`colander.Mapping`
type does the equivalent of ``mapping.get(keyname,
colander.default)`` to find a subvalue during serialization.
.. _deserializing_default_and_null:
Deserializing Default and Null Values
-------------------------------------
It is possible to deserialize both the default and null values.
.. _deserializing_default:
Deserializing The :attr:`colander.default` Value
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The data structure passed to :meth:`colander.SchemaNode.deserialize`
may contain one or more :attr:`colander.default` sentinel markers.
When a :attr:`colander.default` sentinel marker is passed to the
:meth:`colander.SchemaNode.deserialize` method of a particular node in
a schema, the node will take the following steps:
- If the schema node has a valid ``missing`` attribute (the node's
constructor was supplied with a ``missing`` argument), the
``missing`` value will be returned. Note that when this happens,
the ``missing`` value is not validated by any schema node validator:
it is simply returned.
- If the schema node does *not* have a valid ``missing`` attribute
(the node's constructor was not supplied with a ``missing`` value),
a :exc:`colander.Invalid` exception will be raised with a message
indicating that the field is required.
.. note:: There are differences between serialization and
deserialization involving the :attr:`colander.default` value.
During serialization, if an :attr:`colander.default` value is
encountered, and no valid ``default`` attribute exists on the node
related to the value, a :attr:`colander.null` attribute is
returned. The the first difference: deserialization doesn't use
the ``default`` attribute of the node to find a default value in
the same circumstance; instead it uses the ``missing`` attribute.
The second difference: if, during deserialization, an
:attr:`colander.default` value is encountered as the value passed
to the deserialize method, and no valid ``missing`` value exists
for the node, a :exc:`colander.Invalid` exception is raised
(:attr:`colander.null` is not returned, as it is during
serialization).
.. _deserializing_null:
Deserializing The :attr:`colander.null` Value
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The value :attr:`colander.null` has special meaning to types during
deserialization. If :attr:`colander.null` is used as a
deserialization value to a type, it signals that the type should
deserialize the type-specific *null value*.
Deserialization of a *null value* is completely type-specific, meaning
each type is free to deserialize :attr:`colander.null` to a value that
makes sense for that particular type. For example, the
deserialization of a :class:`colander.String` type is the empty
string.
The :attr:`colander.null` value will be passed to a type either
directly or indirectly:
- directly: because :attr:`colander.null` is passed directly to the
``deserialize`` method of a node.
- indirectly: because a node uses a :attr:`colander.null` value as its
``missing`` attribute and the value passed to the serialize method
of a node is missing or :attr:`colander.default`.
When a particular type cannot deserialize the null value to anything
sensible, the type's deserialize method must return the null object
itself as a serialization.
For example, when the :class:`colander.Boolean` type is asked to
deserialize the :attr:`colander.null` value, its ``deserialize``
method simply returns the :attr:`colander.null` value (because null is
conceptually neither true nor false). Therefore, when
:attr:`colander.null` is used as input to deserialization, or as the
``missing`` value of a schema node, it is possible that the
:attr:`colander.null` value will be placed into the deserialized data
structure. The consumer of the deserialization must anticipate this
and deal with the special :attr:`colander.null` value in the output
however it sees fit.
Note that deserialization of the null value never invokes a validator.
Deserialization Combinations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To reduce the potential for confusion about the difference between
:attr:`colander.default` and :attr:`colander.null` during
deserialization, here's a table of serialization combinations. Within
this table, the ``Value`` column represents the value passed to the
:meth:`colander.SchemaNode.deserialize` method of a particular schema
node, the ``Missing`` column represents the ``missing`` value of that
schema node, and the ``Result`` column is a description of the result
of invoking the :meth:`colander.SchemaNode.deserialize` method of the
schema node with the effective value.
===================== ===================== ===========================
Value Missing Result
===================== ===================== ===========================
colander.default <missing> Invalid exception raised
<missing> <missing> Invalid exception raised
colander.default value value deserialized
<missing> value value deserialized
colander.default colander.null null deserialized
<missing> colander.null null deserialized
value <missing> value deserialized
value_a value_b value_a deserialized
value colander.null value deserialized
colander.null <missing> null deserialized
colander.null value null deserialized
colander.null colander.null null deserialized
===================== ===================== ===========================
.. note:: ``<missing>`` in the above table represents the circumstance
in which a key present in a :class:`colander.MappingSchema` is not
present in a mapping passed to its
:meth:`colander.SchemaNode.deserialize` method. In reality,
``<missing>`` means exactly the same thing as
:attr:`colander.default`, because the :class:`colander.Mapping`
type does the equivalent of ``mapping.get(keyname,
colander.default)`` to find a subvalue during deserialization.

View File

@@ -1,8 +1,10 @@
Extending Colander Extending Colander
================== ==================
You can extend Colander by defining a new type or defining a new You can extend Colander by defining a new :term:`type` or by defining
validator. a new :term:`validator`.
.. _defining_a_new_type:
Defining a New Type Defining a New Type
------------------- -------------------
@@ -15,12 +17,10 @@ Python data structure (a :term:`appstruct`).
Here's a type which implements boolean serialization and Here's a type which implements boolean serialization and
deserialization. It serializes a boolean to the string ``true`` or deserialization. It serializes a boolean to the string ``true`` or
``false``; it deserializes a string (presumably ``true`` or ``false``, ``false`` or the special :attr:`colander.null` sentinel; it then
but allows some wiggle room for ``t``, ``on``, ``yes``, ``y``, and deserializes a string (presumably ``true`` or ``false``, but allows
``1``) to a boolean value. It deals with the sentinel value some wiggle room for ``t``, ``on``, ``yes``, ``y``, and ``1``) to a
:attr:`colander.null` by simply returning it when asked to serialize boolean value.
or deserialize it: during serialization, the caller will need to
anticipate this.
.. code-block:: python .. code-block:: python
:linenos: :linenos:
@@ -36,8 +36,6 @@ anticipate this.
return appstruct and 'true' or 'false' return appstruct and 'true' or 'false'
def deserialize(self, node, cstruct): def deserialize(self, node, cstruct):
if cstruct is null:
return null
if not isinstance(cstruct, basestring): if not isinstance(cstruct, basestring):
raise Invalid(node, '%r is not a string' % cstruct) raise Invalid(node, '%r is not a string' % cstruct)
value = cstruct.lower() value = cstruct.lower()
@@ -45,6 +43,13 @@ anticipate this.
return True return True
return False return False
Note that the ``deserialize`` method of a type does not need to
explicitly deserialize the :attr:`colander.null` value.
Deserialization of the null value is dealt with at a higher level
(with the :meth:`colander.SchemaNode.deserialize` method); a type will
never receive an :attr:`colander.null` value as a ``cstruct`` argument
to its ``deserialize`` method.
Here's how you would use the resulting class as part of a schema: Here's how you would use the resulting class as part of a schema:
.. code-block:: python .. code-block:: python
@@ -61,14 +66,16 @@ defined in the ``Boolean`` type class.
Note that the only two real constraints of a type class are: Note that the only two real constraints of a type class are:
- its ``serialize`` method must be able to make sense of a value
generated by its ``deserialize`` method and vice versa.
- it must deal specially with the value :attr:`colander.null` within - it must deal specially with the value :attr:`colander.null` within
both ``serialize`` and ``deserialize``, either returning it or ``serialize``, translating it to a type-specific null value.
translating it to a type-specific null value.
The serialize and method of a type accept two values: ``node``, and - its ``serialize`` method must be able to make sense of a value
generated by its ``deserialize`` method and vice versa, except that
the ``deserialize`` method needn't deal with the
:attr:`colander.null` value specially even if the ``serialize``
method returns it.
The ``serialize`` method of a type accepts two values: ``node``, and
``appstruct``. ``node`` will be the schema node associated with this ``appstruct``. ``node`` will be the schema node associated with this
type. It is used when the type must raise a :exc:`colander.Invalid` type. It is used when the type must raise a :exc:`colander.Invalid`
error, which expects a schema node as its first constructor argument. error, which expects a schema node as its first constructor argument.
@@ -82,9 +89,22 @@ error, which expects a schema node as its first constructor argument.
``cstruct`` will be the :term:`cstruct` value that needs to be ``cstruct`` will be the :term:`cstruct` value that needs to be
deserialized. deserialized.
A type class does not need to implement a constructor (``__init__``),
but it isn't prevented from doing so if it needs to accept arguments;
Colander itself doesn't construct any types, only users of Colander
schemas do, so how types are constructed is beyond the scope of
Colander itself.
The :exc:`colander.Invalid` exception may be raised during
serialization or deserialization as necessary for whatever reason the
type feels appropriate (the inability to serialize or deserialize a
value being the most common case).
For a more formal definition of a the interface of a type, see For a more formal definition of a the interface of a type, see
:class:`colander.interfaces.Type`. :class:`colander.interfaces.Type`.
.. _defining_a_new_validator:
Defining a New Validator Defining a New Validator
------------------------ ------------------------

View File

@@ -12,22 +12,37 @@ Glossary
consumed by the :meth:`colander.SchemaNode.deserialize` method. consumed by the :meth:`colander.SchemaNode.deserialize` method.
appstruct appstruct
A raw application data structure (complex Python objects). A raw application data structure (a structure of complex Python
objects), passed to the :meth:`colander.SchemaNode.serialize`
method for serialization. The
:meth:`colander.SchemaNode.deserialize` method accepts a
:term:`cstruct` and returns an appstruct.
schema schema
A nested collection of :term:`schema node` objects representing A nested collection of :term:`schema node` objects representing
an arrangement of data. an arrangement of data.
schema node schema node
A schema node can serialize an :term:`appstruct` to a A schema node is an object which can serialize an
:term:`cstruct` and deserialize a :term:`cstruct` to an :term:`appstruct` to a :term:`cstruct` and deserialize a
:term:`appstruct` (object derived from :term:`appstruct` from a :term:`cstruct` an (object derived from
:class:`colander.SchemaNode` or one of the colander Schema :class:`colander.SchemaNode` or one of the colander Schema
classes). classes).
type
An object representing a particular type of data (mapping,
boolean, string, etc) capable of serializing an :term:`appstruct`
and of deserializing a :term:`cstruct`. Colander has various
built-in types (:class:`colander.String`,
:class:`colander.Mapping`, etc) and may be extended with
additional types (see :ref:`defining_a_new_type`).
validator validator
A Colander validator callable. Accepts a ``node`` A Colander validator callable. Accepts a ``node`` object and a
object and a ``value`` and either raises an ``value`` and either raises an :exc:`colander.Invalid` exception
:exc:`colander.Invalid` exception or returns ``None``. Used as or returns ``None``. Used as the ``validator=`` argument to a
the ``validator=`` argument to a schema node, ensuring that the schema node, ensuring that the input meets the requirements of
input meets the requirements of the schema. the schema. Built-in validators exist in Colander
(e.g. :class:`colander.OneOf`, :class:`colander.Range`, etc), and
new validators can be defined to extend Colander (see
:ref:`defining_a_new_validator`).

View File

@@ -54,7 +54,7 @@ internationalizable.
:maxdepth: 2 :maxdepth: 2
basics.rst basics.rst
defaults.rst null.rst
extending.rst extending.rst
interfaces.rst interfaces.rst
api.rst api.rst

306
docs/null.rst Normal file
View File

@@ -0,0 +1,306 @@
.. _null:
The Null Value
==============
:attr:`colander.null` is a sentinel value which may be passed to
:meth:`colander.SchemaNode.serialize` during serialization or to
:meth:`colander.SchemaNode.deserialize` during deserialization.
During serialization, the use of :attr:`colander.null` indicates that
the :term:`appstruct` value corresponding to the node it's passed to
is missing and the value of the ``default`` attribute of the
corresponding node should be used instead.
During deserialization, the use of :attr:`colander.null` indicates
that the :term:`cstruct` value corresponding to the node it's passed
to is missing, and if possible, the value of the ``missing`` attribute
of the corresponding node should be used instead.
Note that :attr:`colander.null` has no relationship to the built-in
Python ``None`` value.
.. _serializing_null:
Serializing The Null Value
--------------------------
A node will attempt to serialize its *default value* during
:meth:`colander.SchemaNode.serialize` if the value it is passed as an
``appstruct`` argument is the :attr:`colander.null` sentinel value.
The *default value* of a node is specified during schema creation as
its ``default`` attribute / argument. For example, the ``hair_color``
node below has a default value of ``brown``:
.. code-block:: python
import colander
class Person(colander.MappingSchema):
name = colander.SchemaNode(colander.String())
age = colander.SchemaNode(colander.Int(),
validator=colander.Range(0, 200))
hair_color = colander.SchemaNode(colander.String(), default='brown')
Because the ``hair_color`` node is passed a ``default`` value, if the
above schema is used to serialize a mapping that does not have a
``hair_color`` key, the default will be serialized:
.. code-block:: python
schema = Person()
serialized = schema.serialize({'name':'Fred', 'age':20})
Even though we did not include the ``hair_color`` attribute in the
appstruct we fed to ``serialize``, the value of ``serialized`` above
will be ``{'name':'Fred, 'age':'20', 'hair_color':'brown'}``. This is
because a ``default`` value of ``brown`` was provided during schema
node construction for ``hair_color``.
The same outcome would have been true had we fed the schema a mapping
for serialization which had the :attr:`colander.null` sentinel as the
``hair_color`` value:
.. code-block:: python
import colander
schema = Person()
serialized = schema.serialize({'name':'Fred', 'age':20,
'hair_color':colander.null})
When the above is run, the value of ``serialized`` will be
``{'name':'Fred, 'age':'20', 'hair_color':'brown'}`` just as it was in
the example where ``hair_color`` was not present in the mapping.
As we can see, serializations may be done of partial data structures;
the :attr:`colander.null` value is inserted into the serialization
whenever a corresponding value in the data structure being serialized
is missing.
.. note:: The injection of the :attr:`colander.null` value into a
serialization when a default doesn't exist for the corresponding
node is not a behavior shared during both serialization and
deserialization. While a *serialization* can be performed against
a partial data structure without corresponding node defaults, a
*deserialization* cannot be done to partial data without
corresponding node ``missing`` values. When a value is missing
from a data structure being deserialized, and no ``missing`` value
exists for the node corresponding to the missing item in the data
structure, a :class:`colander.Invalid` exception will be the
result.
If, during serialization, a value for the node is missing from the
cstruct and the node does not possess an explicit *default value*, the
:attr:`colander.null` sentinel value is passed to the type's
``serialize`` method directly, instructing the type to serialize a
type-specific *null value*.
Serialization of a null value is completely type-specific, meaning
each type is free to serialize :attr:`colander.null` to a value that
makes sense for that particular type. For example, the null
serialization value of a :class:`colander.String` type is the empty
string.
For example:
.. code-block:: python
import colander
class Person(colander.MappingSchema):
name = colander.SchemaNode(colander.String())
age = colander.SchemaNode(colander.Int(),
validator=colander.Range(0, 200))
hair_color = colander.SchemaNode(colander.String())
schema = Person()
serialized = schema.serialize({'name':'Fred', 'age':20})
In the above example, the ``hair_color`` value is missing and the
schema does *not* name a ``default`` value for ``hair_color``.
However, when we attempt to serialize the data structure, an error is
not raised. Instead, the value for ``serialized`` above will be
``{'name':'Fred, 'age':'20', 'hair_color':colander.null}``.
Because we did not include the ``hair_color`` attribute in the data we
fed to ``serialize``, and there was no ``default`` value associated
with ``hair_color`` to fall back to, the :attr:`colander.null` value
is passed as the ``appstruct`` value to the ``serialize`` method of
the underlying type (:class:`colander.String`). The return value of
that type's ``serialize`` method when :attr:`colander.null` is passed
as the ``appstruct`` is placed into the serialization.
:class:`colander.String` happens to *return* :attr:`colander.null`
when it is passed :attr:`colander.null` as its appstruct argument, so
this is what winds up in the resulting cstruct.
The :attr:`colander.null` value will be passed to a type either
directly or indirectly:
- directly: because :attr:`colander.null` is passed directly to the
``serialize`` method of a node.
- indirectly: because every schema node uses a :attr:`colander.null`
value as its ``default`` attribute when no explicit default is
provided.
When a particular type cannot serialize the null value to anything
sensible, that type's ``serialize`` method must return the null object
itself as a serialization. For example, when the
:class:`colander.Boolean` type is asked to serialize the
:attr:`colander.null` value, its ``serialize`` method simply returns
the :attr:`colander.null` value (because null is conceptually neither
true nor false).
Therefore, when :attr:`colander.null` is used as input to
serialization, or as the default value of a schema node, it is
possible that the :attr:`colander.null` value will placed into the
serialized data structure. The consumer of the serialization must
anticipate this and deal with the special :attr:`colander.null` value
in the output however it sees fit.
Serialization Combinations
~~~~~~~~~~~~~~~~~~~~~~~~~~
Within this table, the ``Value`` column represents the value passed to
the :meth:`colander.SchemaNode.serialize` method of a particular
schema node, the ``Default`` column represents the ``default`` value
of that schema node, and the ``Result`` column is a description of the
result of invoking the :meth:`colander.SchemaNode.serialize` method of
the schema node with the effective value.
===================== ===================== ===========================
Value Default Result
===================== ===================== ===========================
colander.null value value serialized
<missing> value value serialized
colander.null colander.null null serialized
<missing> colander.null null serialized
value <missing> value serialized
value_a value_b value_a serialized
value colander.null value serialized
colander.null <missing> null serialized
colander.null value null serialized
===================== ===================== ===========================
.. note:: ``<missing>`` in the above table represents the circumstance
in which a key present in a :class:`colander.MappingSchema` is not
present in a mapping passed to its
:meth:`colander.SchemaNode.serialize` method. In reality,
``<missing>`` means exactly the same thing as
:attr:`colanderr.null`, because the :class:`colander.Mapping` type
does the equivalent of ``mapping.get(keyname, colander.null)`` to
find a subvalue during serialization.
.. _deserializing_null:
Deserializing The Null Value
----------------------------
The data structure passed to :meth:`colander.SchemaNode.deserialize`
may contain one or more :attr:`colander.null` sentinel markers.
When a :attr:`colander.null` sentinel marker is passed to the
:meth:`colander.SchemaNode.deserialize` method of a particular node in
a schema, the node will take the following steps:
- If the schema node has an explicit ``missing`` attribute (the node's
constructor was supplied with an explicit ``missing`` argument), the
``missing`` value will be returned. Note that when this happens,
the ``missing`` value is not validated by any schema node validator:
it is simply returned.
- If the schema node does *not* have an explicitly provided
``missing`` attribute (the node's constructor was not supplied with
an explicit ``missing`` value), a :exc:`colander.Invalid` exception
will be raised with a message indicating that the field is required.
.. note:: There are differences between serialization and
deserialization involving the :attr:`colander.null` value. During
serialization, if an :attr:`colander.null` value is encountered,
and no valid ``default`` attribute exists on the node related to
the value the *null value* for that node is returned.
Deserialization, however, doesn't use the ``default`` attribute of
the node to find a default deserialization value in the same
circumstance; instead it uses the ``missing`` attribute instead.
Also, if, during deserialization, an :attr:`colander.null` value is
encountered as the value passed to the deserialize method, and no
explicit ``missing`` value exists for the node, a
:exc:`colander.Invalid` exception is raised (:attr:`colander.null`
is not returned, as it is during serialization).
Here's an example of a deserialization which uses a ``missing`` value
in the schema as a deserialization default value:
.. code-block:: python
import colander
class Person(colander.MappingSchema):
name = colander.SchemaNode(colander.String())
age = colander.SchemaNode(colander.Int(), missing=None)
schema = Person()
deserialized = schema.deserialize({'name':'Fred', 'age':colander.null})
The value for ``deserialized`` above will be ``{'name':'Fred,
'age':None}``.
Because the ``age`` schema node is provided a ``missing`` value of
``None``, if that schema is used to deserialize a mapping that has an
an ``age`` key of :attr:`colander.null`, the ``missing`` value of
``None`` is serialized into the appstruct output for ``age``.
.. note:: Note that ``None`` can be used for the ``missing`` schema
node value as required, as in the above example. It's no different
than any other value used as ``missing``.
The :attr:`colander.null` value is also the default, so it needn't be
specified in the cstruct. Therefore, the ``deserialized`` value of
the below is equivalent to the above's:
.. code-block:: python
import colander
class Person(colander.MappingSchema):
name = colander.SchemaNode(colander.String())
age = colander.SchemaNode(colander.Int(), missing=None)
schema = Person()
deserialized = schema.deserialize({'name':'Fred'})
Deserialization Combinations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Within this table, the ``Value`` column represents the value passed to
the :meth:`colander.SchemaNode.deserialize` method of a particular
schema node, the ``Missing`` column represents the ``missing`` value
of that schema node, and the ``Result`` column is a description of the
result of invoking the :meth:`colander.SchemaNode.deserialize` method
of the schema node with the effective value.
===================== ===================== ===========================
Value Missing Result
===================== ===================== ===========================
colander.null <missing> Invalid exception raised
<missing> <missing> Invalid exception raised
colander.null colander.null Invalid exception raised
colander.null value value deserialized
<missing> value value deserialized
value <missing> value deserialized
value colander.null value deserialized
value_a value_b value_a deserialized
===================== ===================== ===========================
.. note:: ``<missing>`` in the above table represents the circumstance
in which a key present in a :class:`colander.MappingSchema` is not
present in a mapping passed to its
:meth:`colander.SchemaNode.deserialize` method. In reality,
``<missing>`` means exactly the same thing as
:attr:`colander.null`, because the :class:`colander.Mapping`
type does the equivalent of ``mapping.get(keyname,
colander.null)`` to find a subvalue during deserialization.