- Undocumented internal API for all type objects: `flatten`.

External type objects should now inherit from
  ``colander.SchemaType`` to get a default implementation.
This commit is contained in:
Chris McDonough
2010-10-07 00:48:03 +00:00
3 changed files with 202 additions and 26 deletions

View File

@@ -21,6 +21,9 @@ Changes
- If ``default`` is deferred, the serialization default will be - If ``default`` is deferred, the serialization default will be
assumed to be ``colander.null``. assumed to be ``colander.null``.
- Undocumented internal API for all type objects: ``flatten``.
External type objects should now inherit from
``colander.SchemaType`` to get a default implementation.
0.8 (2010/09/08) 0.8 (2010/09/08)
----------------- -----------------

View File

@@ -329,7 +329,15 @@ class OneOf(object):
mapping={'val':value, 'choices':choices}) mapping={'val':value, 'choices':choices})
raise Invalid(node, err) raise Invalid(node, err)
class Mapping(object): class SchemaType(object):
""" Base class for all schema types """
def flatten(self, node, appstruct, prefix=''):
result = {}
selfname = '%s%s' % (prefix, node.name)
result[selfname] = appstruct
return result
class Mapping(SchemaType):
""" A type which represents a mapping of names to nodes. """ A type which represents a mapping of names to nodes.
The subnodes of the :class:`colander.SchemaNode` that wraps The subnodes of the :class:`colander.SchemaNode` that wraps
@@ -447,6 +455,19 @@ class Mapping(object):
return self._impl(node, cstruct, callback) return self._impl(node, cstruct, callback)
def flatten(self, node, appstruct, prefix=''):
result = {}
selfname = '%s%s' % (prefix, node.name)
selfprefix = selfname + '.'
result[selfname] = appstruct
for num, subnode in enumerate(node.children):
name = subnode.name
substruct = appstruct.get(name, null)
result.update(subnode.typ.flatten(subnode, substruct,
prefix=selfprefix))
return result
class Positional(object): class Positional(object):
""" """
Marker abstract base class meaning 'this type has children which Marker abstract base class meaning 'this type has children which
@@ -455,7 +476,7 @@ class Positional(object):
creating a dictionary representation of an error tree. creating a dictionary representation of an error tree.
""" """
class Tuple(Positional): class Tuple(Positional, SchemaType):
""" A type which represents a fixed-length sequence of nodes. """ A type which represents a fixed-length sequence of nodes.
The subnodes of the :class:`colander.SchemaNode` that wraps The subnodes of the :class:`colander.SchemaNode` that wraps
@@ -523,7 +544,19 @@ class Tuple(Positional):
return self._impl(node, cstruct, callback) return self._impl(node, cstruct, callback)
class Sequence(Positional): def flatten(self, node, appstruct, prefix=''):
result = {}
selfname = '%s%s' % (prefix, node.name)
selfprefix = selfname + '.'
result[selfname] = appstruct
for num, subnode in enumerate(node.children):
substruct = appstruct[num]
result.update(subnode.typ.flatten(subnode, substruct,
prefix=selfprefix))
return result
class Sequence(Positional, SchemaType):
""" """
A type which represents a variable-length sequence of nodes, A type which represents a variable-length sequence of nodes,
all of which must be of the same type. all of which must be of the same type.
@@ -639,9 +672,26 @@ class Sequence(Positional):
return self._impl(node, cstruct, callback, accept_scalar) return self._impl(node, cstruct, callback, accept_scalar)
def flatten(self, node, appstruct, prefix=''):
result = {}
selfname = '%s%s' % (prefix, node.name)
selfprefix = selfname + '.'
result[selfname] = appstruct
childnode = node.children[0]
for num, subval in enumerate(appstruct):
subname = '%s%s' % (selfprefix, num)
result[subname] = subval
subprefix = subname + '.'
result.update(childnode.typ.flatten(childnode, subval,
prefix=subprefix))
return result
Seq = Sequence Seq = Sequence
class String(object): class String(SchemaType):
""" A type representing a Unicode string. """ A type representing a Unicode string.
This type constructor accepts one argument: This type constructor accepts one argument:
@@ -740,10 +790,9 @@ class String(object):
return result return result
Str = String Str = String
class Number(object): class Number(SchemaType):
""" Abstract base class for float, int, decimal """ """ Abstract base class for float, int, decimal """
num = None num = None
@@ -771,7 +820,6 @@ class Number(object):
mapping={'val':cstruct}) mapping={'val':cstruct})
) )
class Integer(Number): class Integer(Number):
""" A type representing an integer. """ A type representing an integer.
@@ -812,7 +860,7 @@ class Decimal(Number):
def num(self, val): def num(self, val):
return decimal.Decimal(str(val)) return decimal.Decimal(str(val))
class Boolean(object): class Boolean(SchemaType):
""" A type representing a boolean object. """ A type representing a boolean object.
During deserialization, a value in the set (``false``, ``0``) will During deserialization, a value in the set (``false``, ``0``) will
@@ -852,7 +900,7 @@ class Boolean(object):
Bool = Boolean Bool = Boolean
class GlobalObject(object): class GlobalObject(SchemaType):
""" A type representing an importable Python object. This type """ A type representing an importable Python object. This type
serializes 'global' Python objects (objects which can be imported) serializes 'global' Python objects (objects which can be imported)
to dotted Python names. to dotted Python names.
@@ -976,8 +1024,7 @@ class GlobalObject(object):
_('The dotted name "${name}" cannot be imported', _('The dotted name "${name}" cannot be imported',
mapping={'name':cstruct})) mapping={'name':cstruct}))
class DateTime(SchemaType):
class DateTime(object):
""" A type representing a Python ``datetime.datetime`` object. """ A type representing a Python ``datetime.datetime`` object.
This type serializes python ``datetime.datetime`` objects to a This type serializes python ``datetime.datetime`` objects to a
@@ -1058,7 +1105,7 @@ class DateTime(object):
mapping={'val':cstruct, 'err':e})) mapping={'val':cstruct, 'err':e}))
return result return result
class Date(object): class Date(SchemaType):
""" A type representing a Python ``datetime.date`` object. """ A type representing a Python ``datetime.date`` object.
This type serializes python ``datetime.date`` objects to a This type serializes python ``datetime.date`` objects to a
@@ -1243,6 +1290,20 @@ class SchemaNode(object):
cstruct = self.typ.serialize(self, appstruct) cstruct = self.typ.serialize(self, appstruct)
return cstruct return cstruct
def flatten(self, appstruct):
""" Create an fstruct from the appstruct based on the schema
represented by this node and return the fstruct. An fstruct
is a flattened representation of an appstruct. An fstruct is
a dictionary; its keys are dotted names. Each dotted name
represents a location in the schema. The values of an fstruct
dictionary are appstruct subvalues."""
flat = self.typ.flatten(self, appstruct)
return flat
def unflatten(self, fstruct):
""" Create an appstruct based on the schema represented by
this node using the fstruct passed. """
def deserialize(self, cstruct=null): 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

View File

@@ -319,6 +319,17 @@ class TestOneOf(unittest.TestCase):
e = invalid_exc(validator, None, None) e = invalid_exc(validator, None, None)
self.assertEqual(e.msg.interpolate(), '"None" is not one of 1, 2') self.assertEqual(e.msg.interpolate(), '"None" is not one of 1, 2')
class TestSchemaType(unittest.TestCase):
def _makeOne(self, *arg, **kw):
from colander import SchemaType
return SchemaType(*arg, **kw)
def test_flatten(self):
node = DummySchemaNode(None, name='node')
typ = self._makeOne()
result = typ.flatten(node, 'appstruct')
self.assertEqual(result, {'node':'appstruct'})
class TestMapping(unittest.TestCase): class TestMapping(unittest.TestCase):
def _makeOne(self, *arg, **kw): def _makeOne(self, *arg, **kw):
@@ -439,6 +450,19 @@ class TestMapping(unittest.TestCase):
result = typ.serialize(node, null) result = typ.serialize(node, null)
self.assertEqual(result, {'a':null}) self.assertEqual(result, {'a':null})
def test_flatten(self):
node = DummySchemaNode(None, name='node')
int1 = DummyType()
int2 = DummyType()
node.children = [
DummySchemaNode(int1, name='a'),
DummySchemaNode(int2, name='b'),
]
typ = self._makeOne()
result = typ.flatten(node, {'a':1, 'b':2})
self.assertEqual(result,
{'node': {'a': 1, 'b': 2}, 'node.appstruct': 2})
class TestTuple(unittest.TestCase): class TestTuple(unittest.TestCase):
def _makeOne(self): def _makeOne(self):
from colander import Tuple from colander import Tuple
@@ -550,6 +574,18 @@ class TestTuple(unittest.TestCase):
self.assertEqual(e.msg, None) self.assertEqual(e.msg, None)
self.assertEqual(len(e.children), 2) self.assertEqual(len(e.children), 2)
def test_flatten(self):
node = DummySchemaNode(None, name='node')
int1 = DummyType()
int2 = DummyType()
node.children = [
DummySchemaNode(int1, name='a'),
DummySchemaNode(int2, name='b'),
]
typ = self._makeOne()
result = typ.flatten(node, (1, 2))
self.assertEqual(result, {'node': (1, 2), 'node.appstruct': 2})
class TestSequence(unittest.TestCase): class TestSequence(unittest.TestCase):
def _makeOne(self, **kw): def _makeOne(self, **kw):
from colander import Sequence from colander import Sequence
@@ -646,6 +682,24 @@ class TestSequence(unittest.TestCase):
self.assertEqual(e.msg, None) self.assertEqual(e.msg, None)
self.assertEqual(len(e.children), 2) self.assertEqual(len(e.children), 2)
def test_flatten(self):
node = DummySchemaNode(None, name='node')
int1 = DummyType()
int2 = DummyType()
node.children = [
DummySchemaNode(int1, name='a'),
DummySchemaNode(int2, name='b'),
]
typ = self._makeOne()
result = typ.flatten(node, [1, 2])
self.assertEqual(
result,
{'node': [1, 2],
'node.0': 1,
'node.0.appstruct': 1,
'node.1.appstruct': 2,
'node.1': 2})
class TestString(unittest.TestCase): class TestString(unittest.TestCase):
def _makeOne(self, encoding=None): def _makeOne(self, encoding=None):
from colander import String from colander import String
@@ -1618,19 +1672,72 @@ class TestFunctional(object):
[{'key':1, 'key2':2}, {'key':3, 'key2':4}]) [{'key':1, 'key2':2}, {'key':3, 'key2':4}])
self.assertEqual(result['tup'], (1, 's')) self.assertEqual(result['tup'], (1, 's'))
def test_flatten_ok(self):
import colander
appstruct = {
'int':10,
'ob':colander.tests,
'seq':[(1, 's'),(2, 's'), (3, 's'), (4, 's')],
'seq2':[{'key':1, 'key2':2}, {'key':3, 'key2':4}],
'tup':(1, 's'),
}
schema = self._makeSchema()
result = schema.flatten(appstruct)
expected = {
'schema.seq.2.tup.tupstring': 's',
'schema.seq2.0.mapping.key2': 2,
'schema.seq.0': (1, 's'),
'schema.seq.1': (2, 's'),
'schema.seq.2': (3, 's'),
'schema.seq.3': (4, 's'),
'schema.seq': [(1, 's'), (2, 's'), (3, 's'), (4, 's')],
'schema.ob': colander.tests,
'schema.seq2.1.mapping.key2': 4,
'schema.seq.0.tup': (1, 's'),
'schema.seq.1.tup': (2, 's'),
'schema.seq2.0.mapping': {'key2': 2, 'key': 1},
'schema.seq2.1.mapping': {'key2': 4, 'key': 3},
'schema.seq.1.tup.tupstring': 's',
'schema.seq2.0.mapping.key': 1,
'schema.seq.1.tup.tupint': 2,
'schema.tup': (1, 's'),
'schema.seq.3.tup': (4, 's'),
'schema.seq.0.tup.tupstring': 's',
'schema.seq.2.tup': (3, 's'),
'schema.seq.3.tup.tupstring': 's',
'schema.seq.3.tup.tupint': 4,
'schema.seq2.1.mapping.key': 3,
'schema.int': 10,
'schema.seq2.0': {'key2': 2, 'key': 1},
'schema.seq.0.tup.tupint': 1,
'schema.tup.tupint': 1,
'schema.tup.tupstring': 's',
'schema.seq.2.tup.tupint': 3,
'schema.seq2': [{'key2': 2, 'key': 1}, {'key2': 4, 'key': 3}],
'schema.seq2.1': {'key2': 4, 'key': 3},
'schema': {'int': 10,
'seq2': [{'key2': 2, 'key': 1}, {'key2': 4, 'key': 3}],
'tup': (1, 's'),
'ob':colander.tests,
'seq': [(1, 's'), (2, 's'), (3, 's'), (4, 's')]}}
for k, v in result.items():
self.assertEqual(v, expected[k])
def test_invalid_asdict(self): def test_invalid_asdict(self):
expected = { expected = {
'int': '20 is greater than maximum value 10', 'schema.int': '20 is greater than maximum value 10',
'ob': 'The dotted name "no.way.this.exists" cannot be imported', 'schema.ob': 'The dotted name "no.way.this.exists" cannot be imported',
'seq.0.0': '"q" is not a number', 'schema.seq.0.0': '"q" is not a number',
'seq.1.0': '"w" is not a number', 'schema.seq.1.0': '"w" is not a number',
'seq.2.0': '"e" is not a number', 'schema.seq.2.0': '"e" is not a number',
'seq.3.0': '"r" is not a number', 'schema.seq.3.0': '"r" is not a number',
'seq2.0.key': '"t" is not a number', 'schema.seq2.0.key': '"t" is not a number',
'seq2.0.key2': '"y" is not a number', 'schema.seq2.0.key2': '"y" is not a number',
'seq2.1.key': '"u" is not a number', 'schema.seq2.1.key': '"u" is not a number',
'seq2.1.key2': '"i" is not a number', 'schema.seq2.1.key2': '"i" is not a number',
'tup.0': '"s" is not a number'} 'schema.tup.0': '"s" is not a number'}
data = { data = {
'int':'20', 'int':'20',
'ob':'no.way.this.exists', 'ob':'no.way.this.exists',
@@ -1701,7 +1808,8 @@ class TestImperative(unittest.TestCase, TestFunctional):
ob, ob,
tup, tup,
seq, seq,
seq2) seq2,
name='schema')
return schema return schema
@@ -1721,7 +1829,7 @@ class TestDeclarative(unittest.TestCase, TestFunctional):
key2 = colander.SchemaNode(colander.Int()) key2 = colander.SchemaNode(colander.Int())
class SequenceOne(colander.SequenceSchema): class SequenceOne(colander.SequenceSchema):
tuple = TupleSchema() tup = TupleSchema()
class SequenceTwo(colander.SequenceSchema): class SequenceTwo(colander.SequenceSchema):
mapping = MappingSchema() mapping = MappingSchema()
@@ -1734,7 +1842,7 @@ class TestDeclarative(unittest.TestCase, TestFunctional):
tup = TupleSchema() tup = TupleSchema()
seq2 = SequenceTwo() seq2 = SequenceTwo()
schema = MainSchema() schema = MainSchema(name='schema')
return schema return schema
class Test_null(unittest.TestCase): class Test_null(unittest.TestCase):
@@ -1792,3 +1900,7 @@ class DummyType(object):
def deserialize(self, node, value): def deserialize(self, node, value):
return value return value
def flatten(self, node, appstruct, prefix=''):
key = prefix + 'appstruct'
return {key:appstruct}