diff --git a/CHANGES.txt b/CHANGES.txt index 50aaeb3..0252ff7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -14,12 +14,20 @@ Next release - Add a ``__setitem__`` method to the ``colander.Invalid`` class. +- The ``colander.Mapping`` keyword argument ``unknown_keys`` has been + renamed to ``unknown``. + - Allow ``colander.Mapping`` type to accept a new constructor - argument: ``missing``. + argument: ``partial``. - Allow ``colander.Mapping.serialize`` and ``colander.Mapping.deserialize`` methods to accept ``unknown`` and - ``serialize`` keyword arguments. + ``partial`` keyword arguments. + +- New interface methods required by types and schema nodes: + ``pserialize`` and ``pdeserialize``. These partially serialize or + partially deserialize a value (the definition of "partial" is up to + the type). 0.5 (2010-03-31) ---------------- diff --git a/colander/__init__.py b/colander/__init__.py index 4006c64..8dfc4cd 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -189,49 +189,57 @@ class OneOf(object): raise Invalid(node, '"%s" is not one of %s' % ( value, ', '.join(['%s' % x for x in self.values]))) -class Mapping(object): +class Type(object): + """ Abstract base class for types (only for convenience) """ + def pserialize(self, node, value): + return self.serialize(node, value) + + def pdeserialize(self, node, value): + return self.deserialize(node, value) + +class Mapping(Type): """ A type which represents a mapping of names to nodes. The subnodes of the :class:`colander.SchemaNode` that wraps this type imply the named keys and values in the mapping. The constructor of this type accepts two extra optional keyword - arguments that other types do not: ``unknown`` and ``missing``. + arguments that other types do not: ``unknown`` and ``partial``. unknown - ``unknown`` controls the behavior of this type when an unknown - key is encountered in the value passed to the ``serialize`` or - ``deserialize`` methods of this instance. The potential values - of ``unknown`` are: + ``unknown`` controls the behavior of this type when an unknown + key is encountered in the value passed to the ``serialize`` or + ``deserialize`` methods of this instance. The potential + values of ``unknown`` are: - - ``ignore`` means that keys that are not present in the schema - associated with this type will be ignored during - deserialization. + - ``ignore`` means that keys that are not present in the schema + associated with this type will be ignored during + deserialization. - - ``raise`` will cause a :exc:`colander.Invalid` exception to be - raised when unknown keys are present during deserialization. + - ``raise`` will cause a :exc:`colander.Invalid` exception to be + raised when unknown keys are present during deserialization. - - ``preserve`` will preserve the 'raw' unknown keys and values - in the returned data structure. + - ``preserve`` will preserve the 'raw' unknown keys and values + in the returned data structure. - Default: ``ignore``. + Default: ``ignore``. - missing - ``missing`` controls the behavior of this type when a + partial + ``partial`` controls the behavior of this type when a schema-expected key is missing from the value passed to the ``serialize`` and ``deserialize`` methods of this instance. - During serialization and deserialization, when ``missing`` is - ``raise``, a :exc:`colander.Invalid` exception will be raised + During serialization and deserialization, when ``partial`` is + ``False``, a :exc:`colander.Invalid` exception will be raised if the mapping value does not contain a key specified by the - schema node related to this mapping type. When ``missing`` is - ``ignore``, no exception is raised and a partial mapping will + schema node related to this mapping type. When ``partial`` is + ``True``, no exception is raised and a partial mapping will be serialized/deserialized. Default: ``raise``. """ - def __init__(self, unknown='ignore', missing='raise'): - self.missing = self._check_missing(missing) + def __init__(self, unknown='ignore', partial=False): + self.partial = partial self.unknown = self._check_unknown(unknown) def _check_unknown(self, unknown): @@ -241,23 +249,15 @@ class Mapping(object): 'or "preserve"') return unknown - def _check_missing(self, missing): - if not missing in ['ignore', 'raise']: - raise ValueError( - 'missing argument must be one of "ignore" or "raise"') - return missing - def _validate(self, node, value): try: return dict(value) except Exception, e: raise Invalid(node, '%r is not a mapping type: %s' % (value, e)) - def _impl(self, node, value, callback, default_callback, unknown, missing): - if missing is None: - missing = self.missing - else: - missing = self._check_missing(missing) + def _impl(self, node, value, callback, default_callback, unknown, partial): + if partial is None: + partial = self.partial if unknown is None: unknown = self.unknown @@ -276,7 +276,7 @@ class Mapping(object): try: if subval is _missing: if subnode.required: - if missing == 'raise': + if not partial: raise Invalid( subnode, '"%s" is required but missing' % subnode.name) @@ -303,12 +303,12 @@ class Mapping(object): return result - def deserialize(self, node, value, unknown=None, missing=None): + def deserialize(self, node, value, unknown=None, partial=None): """ Along with the normal ``node`` and ``value`` arguments, this method implementation accepts two additional optional - arguments that other type implementations do not: ``missing`` - and ``raise``. These arguments can be used to override the + arguments that other type implementations do not: ``unknown`` + and ``partial``. These arguments can be used to override the instance defaults of the same name for the duration of a particular serialization or deserialization. @@ -319,12 +319,12 @@ class Mapping(object): this instance. It defaults to ``None``, which signifies that the instance default should be used. - missing - If this value is provided, it must be one of ``raise`` or - ``ignore``, overriding the behavior implied by the value set - by the ``missing`` argument to constructor of this instance. - It defaults to ``None``, which signifies that the instance - default should be used. + partial + If this value is provided, it must be a boolean, overriding + the behavior implied by the value set by the ``partial`` + argument to constructor of this instance. It defaults to + ``None``, which signifies that the instance default should + be used. """ def callback(subnode, subval): @@ -332,14 +332,14 @@ class Mapping(object): def default_callback(subnode): return subnode.default return self._impl( - node, value, callback, default_callback, unknown, missing) + node, value, callback, default_callback, unknown, partial) - def serialize(self, node, value, unknown=None, missing=None): + def serialize(self, node, value, unknown=None, partial=None): """ Along with the normal ``node`` and ``value`` arguments, this method implementation accepts two additional optional - arguments that other type implementations do not: ``missing`` - and ``raise``. These arguments can be used to override the + arguments that other type implementations do not: ``unknown`` + and ``partial``. These arguments can be used to override the instance defaults of the same name for the duration of a particular serialization or deserialization. @@ -350,12 +350,12 @@ class Mapping(object): this instance. It defaults to ``None``, which signifies that the instance default should be used. - missing - If this value is provided, it must be one of ``raise`` or - ``ignore``, overriding the behavior implied by the value set - by the ``missing`` argument to constructor of this instance. - It defaults to ``None``, which signifies that the instance - default should be used. + partial + If this value is provided, it must be a boolean, overriding + the behavior implied by the value set by the ``partial`` + argument to constructor of this instance. It defaults to + ``None``, which signifies that the instance default should + be used. """ def callback(subnode, subval): return subnode.serialize(subval) @@ -363,7 +363,13 @@ class Mapping(object): return subnode.serialize(subnode.default) return self._impl( - node, value, callback, default_callback, unknown, missing) + node, value, callback, default_callback, unknown, partial) + + def pserialize(self, node, value): + return self.serialize(node, value, partial=True) + + def pdeserialize(self, node, value): + return self.serialize(node, value, partial=True) class Positional(object): """ @@ -373,7 +379,7 @@ class Positional(object): creating a dictionary representation of an error tree. """ -class Tuple(Positional): +class Tuple(Type, Positional): """ A type which represents a fixed-length sequence of nodes. The subnodes of the :class:`colander.SchemaNode` that wraps @@ -427,7 +433,7 @@ class Tuple(Positional): return subnode.serialize(subval) return self._impl(node, value, callback) -class Sequence(Positional): +class Sequence(Type, Positional): """ A type which represents a variable-length sequence of nodes, all of which must be of the same type. @@ -535,7 +541,7 @@ Seq = Sequence default_encoding = 'utf-8' -class String(object): +class String(Type): """ A type representing a Unicode string. This type constructor accepts a single argument ``encoding``, representing the encoding which should be applied to object serialization. It defaults to @@ -568,7 +574,7 @@ class String(object): Str = String -class Integer(object): +class Integer(Type): """ A type representing an integer. The subnodes of the :class:`colander.SchemaNode` that wraps @@ -592,7 +598,7 @@ class Integer(object): Int = Integer -class Float(object): +class Float(Type): """ A type representing a float. The subnodes of the :class:`colander.SchemaNode` that wraps @@ -616,7 +622,7 @@ class Float(object): Int = Integer -class Boolean(object): +class Boolean(Type): """ A type representing a boolean object. During deserialization, a value in the set (``false``, ``0``) will @@ -649,7 +655,7 @@ class Boolean(object): Bool = Boolean -class GlobalObject(object): +class GlobalObject(Type): """ A type representing an importable Python object. This type serializes 'global' Python objects (objects which can be imported) to dotted Python names. @@ -756,7 +762,7 @@ class GlobalObject(object): except AttributeError: raise Invalid(node, '%r has no __name__' % value) -class DateTime(object): +class DateTime(Type): """ A type representing a Python ``datetime.datetime`` object. This type serializes python ``datetime.datetime`` objects to a @@ -822,7 +828,7 @@ class DateTime(object): 'exc':e}) return result -class Date(object): +class Date(Type): """ A type representing a Python ``datetime.date`` object. This type serializes python ``datetime.date`` objects to a @@ -833,7 +839,7 @@ class Date(object): You can adjust the error message reported by this class by changing its ``err_template`` attribute in a subclass on an - instance of this class. By default, the ``err_tempalte`` + instance of this class. By default, the ``err_template`` attribute is the string ``%(value)s cannot be parsed as an iso8601 date: %(exc)s``. This string is used as the interpolation subject of a dictionary composed of ``value`` and ``exc``. ``value`` and @@ -946,20 +952,35 @@ class SchemaNode(object): return self.typ.serialize(self, self.default) def deserialize(self, value): - """ Derialize the value based on the schema represented by this - node """ + """ Deserialize the value based on the schema represented by + this node. The values passed as ``kw`` will be passed along + to the ``deserialize`` method of this node's type.""" value = self.typ.deserialize(self, value) if self.validator is not None: self.validator(self, value) return value def serialize(self, value): - """ Serialize the value based on the schema represented by this - node """ + """ Serialize the value based on the schema represented by + this node. The values passed as ``kw`` will be passed along + to the ``serialize`` method of this node's type.""" return self.typ.serialize(self, value) + def pserialize(self, value): + """ Partially serialize the value based on the schema + represented by this node. The values passed as ``kw`` will be + passed along to the ``pserialize`` method of this node's type.""" + return self.typ.pserialize(self, value) + + def pdeserialize(self, value): + """ Partially deserialize the value based on the schema + represented by this node. The values passed as ``kw`` will be + passed along to the ``pdeserialize`` method of this node's + type.""" + return self.typ.pdeserialize(self, value) + def add(self, node): - """ Add a subnode to this node """ + """ Add a subnode to this node. """ self.children.append(node) def __getitem__(self, name): diff --git a/colander/interfaces.py b/colander/interfaces.py index a9334b6..7fcc71b 100644 --- a/colander/interfaces.py +++ b/colander/interfaces.py @@ -34,7 +34,6 @@ class Type(object): def deserialize(self, struct, value): """ - Deserialze the serialization represented by ``value`` to a data structure. The deserialization should be composed of one or more objects which can be serialized by the @@ -53,3 +52,28 @@ class Type(object): raised. """ + def pserialize(self, node, value): + """ Partially serialize a value, ignoring any missing + components. + + The description of the ``node`` and ``value`` arguments are + the same as those provided to ``serialize``. + + The return value and behavior of any partial serialization is + completely type-dependent. If partial serialization is not + applicable for a type, this method will usually be an alias + for that type's 'serialize' method. + """ + + def pdeserialize(self, node, value): + """ Partially deserialize a value, ignoring any missing + components. + + The description of the ``node`` and ``value`` arguments are + the same as those provided to ``deserialize``. + + The return value and behavior of any partial deserialization + is completely type-dependent. If partial deserialization is + not applicable for a type, this method will usually be an + alias for that type's 'deserialize' method. + """ diff --git a/colander/tests.py b/colander/tests.py index ed82d83..e1cd9c0 100644 --- a/colander/tests.py +++ b/colander/tests.py @@ -211,6 +211,23 @@ class TestOneOf(unittest.TestCase): e = invalid_exc(validator, None, None) self.assertEqual(e.msg, '"None" is not one of 1, 2') +class TestType(unittest.TestCase): + def _makeOne(self): + from colander import Type + return Type() + + def test_pserialize(self): + typ = self._makeOne() + typ.serialize = lambda *arg: arg + result = typ.pserialize(None, None) + self.assertEqual(result, (None, None)) + + def test_pdeserialize(self): + typ = self._makeOne() + typ.deserialize = lambda *arg: arg + result = typ.pdeserialize(None, None) + self.assertEqual(result, (None, None)) + class TestMapping(unittest.TestCase): def _makeOne(self, *arg, **kw): from colander import Mapping @@ -227,16 +244,6 @@ class TestMapping(unittest.TestCase): except ValueError, e: # pragma: no cover raise AssertionError(e) - def test_ctor_bad_missing(self): - self.assertRaises(ValueError, self._makeOne, 'ignore', 'badarg') - - def test_ctor_good_missing(self): - try: - self._makeOne(missing='ignore') - self._makeOne(missing='raise') - except ValueError, e: # pragma: no cover - raise AssertionError(e) - def test_deserialize_not_a_mapping(self): node = DummySchemaNode(None) typ = self._makeOne() @@ -316,24 +323,24 @@ class TestMapping(unittest.TestCase): e = invalid_exc(typ.deserialize, node, {'a':1}) self.assertEqual(e.children[0].msg, '"b" is required but missing') - def test_deserialize_subnode_missing_ignore(self): + def test_deserialize_subnode_partial(self): node = DummySchemaNode(None) node.children = [ DummySchemaNode(None, name='a'), DummySchemaNode(None, name='b'), ] - typ = self._makeOne(missing='ignore') + typ = self._makeOne(partial=True) result = typ.deserialize(node, {'a':1}) self.assertEqual(result, {'a':1}) - def test_deserialize_subnode_missing_ignore_override(self): + def test_deserialize_subnode_partial_ignore_override(self): node = DummySchemaNode(None) node.children = [ DummySchemaNode(None, name='a'), DummySchemaNode(None, name='b'), ] typ = self._makeOne() - result = typ.deserialize(node, {'a':1}, 'ignore', 'ignore') + result = typ.deserialize(node, {'a':1}, 'ignore', True) self.assertEqual(result, {'a':1}) def test_serialize_not_a_mapping(self): @@ -415,24 +422,44 @@ class TestMapping(unittest.TestCase): e = invalid_exc(typ.serialize, node, {'a':1}) self.assertEqual(e.children[0].msg, '"b" is required but missing') - def test_serialize_subnode_missing_ignore(self): + def test_serialize_subnode_partial(self): node = DummySchemaNode(None) node.children = [ DummySchemaNode(None, name='a'), DummySchemaNode(None, name='b'), ] - typ = self._makeOne(missing='ignore') + typ = self._makeOne(partial=True) result = typ.serialize(node, {'a':1}) self.assertEqual(result, {'a':1}) - def test_serialize_subnode_missing_ignore_override(self): + def test_serialize_subnode_partial_ignore_override(self): node = DummySchemaNode(None) node.children = [ DummySchemaNode(None, name='a'), DummySchemaNode(None, name='b'), ] typ = self._makeOne() - result = typ.serialize(node, {'a':1}, 'ignore', 'ignore') + result = typ.serialize(node, {'a':1}, 'ignore', True) + self.assertEqual(result, {'a':1}) + + def test_pserialize(self): + node = DummySchemaNode(None) + node.children = [ + DummySchemaNode(None, name='a'), + DummySchemaNode(None, name='b'), + ] + typ = self._makeOne() + result = typ.pserialize(node, {'a':1}) + self.assertEqual(result, {'a':1}) + + def test_pdeserialize(self): + node = DummySchemaNode(None) + node.children = [ + DummySchemaNode(None, name='a'), + DummySchemaNode(None, name='b'), + ] + typ = self._makeOne() + result = typ.pdeserialize(node, {'a':1}) self.assertEqual(result, {'a':1}) class TestTuple(unittest.TestCase): @@ -1200,12 +1227,24 @@ class TestSchemaNode(unittest.TestCase): e = invalid_exc(node.deserialize, 1) self.assertEqual(e.msg, 'Wrong') + def test_pdeserialize(self): + typ = DummyType() + node = self._makeOne(typ) + result = node.pdeserialize(1) + self.assertEqual(result, 1) + def test_serialize(self): typ = DummyType() node = self._makeOne(typ) result = node.serialize(1) self.assertEqual(result, 1) + def test_pserialize(self): + typ = DummyType() + node = self._makeOne(typ) + result = node.pserialize(1) + self.assertEqual(result, 1) + def test_add(self): node = self._makeOne(None) node.add(1) @@ -1473,4 +1512,10 @@ class DummyType(object): def deserialize(self, node, value): return value + + def pserialize(self, node, value): + return value + def pdeserialize(self, node, value): + return value +