- 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
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)
-----------------

View File

@@ -329,7 +329,15 @@ class OneOf(object):
mapping={'val':value, 'choices':choices})
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.
The subnodes of the :class:`colander.SchemaNode` that wraps
@@ -447,6 +455,19 @@ class Mapping(object):
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):
"""
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.
"""
class Tuple(Positional):
class Tuple(Positional, SchemaType):
""" A type which represents a fixed-length sequence of nodes.
The subnodes of the :class:`colander.SchemaNode` that wraps
@@ -523,7 +544,19 @@ class Tuple(Positional):
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,
all of which must be of the same type.
@@ -639,9 +672,26 @@ class Sequence(Positional):
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
class String(object):
class String(SchemaType):
""" A type representing a Unicode string.
This type constructor accepts one argument:
@@ -740,10 +790,9 @@ class String(object):
return result
Str = String
class Number(object):
class Number(SchemaType):
""" Abstract base class for float, int, decimal """
num = None
@@ -771,7 +820,6 @@ class Number(object):
mapping={'val':cstruct})
)
class Integer(Number):
""" A type representing an integer.
@@ -812,7 +860,7 @@ class Decimal(Number):
def num(self, val):
return decimal.Decimal(str(val))
class Boolean(object):
class Boolean(SchemaType):
""" A type representing a boolean object.
During deserialization, a value in the set (``false``, ``0``) will
@@ -852,7 +900,7 @@ class Boolean(object):
Bool = Boolean
class GlobalObject(object):
class GlobalObject(SchemaType):
""" A type representing an importable Python object. This type
serializes 'global' Python objects (objects which can be imported)
to dotted Python names.
@@ -976,8 +1024,7 @@ class GlobalObject(object):
_('The dotted name "${name}" cannot be imported',
mapping={'name':cstruct}))
class DateTime(object):
class DateTime(SchemaType):
""" A type representing a Python ``datetime.datetime`` object.
This type serializes python ``datetime.datetime`` objects to a
@@ -1058,7 +1105,7 @@ class DateTime(object):
mapping={'val':cstruct, 'err':e}))
return result
class Date(object):
class Date(SchemaType):
""" A type representing a Python ``datetime.date`` object.
This type serializes python ``datetime.date`` objects to a
@@ -1243,6 +1290,20 @@ class SchemaNode(object):
cstruct = self.typ.serialize(self, appstruct)
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):
""" Deserialize and validate the :term:`cstruct` into an
: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)
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):
def _makeOne(self, *arg, **kw):
@@ -439,6 +450,19 @@ class TestMapping(unittest.TestCase):
result = typ.serialize(node, 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):
def _makeOne(self):
from colander import Tuple
@@ -550,6 +574,18 @@ class TestTuple(unittest.TestCase):
self.assertEqual(e.msg, None)
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):
def _makeOne(self, **kw):
from colander import Sequence
@@ -646,6 +682,24 @@ class TestSequence(unittest.TestCase):
self.assertEqual(e.msg, None)
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):
def _makeOne(self, encoding=None):
from colander import String
@@ -1617,20 +1671,73 @@ class TestFunctional(object):
self.assertEqual(result['seq2'],
[{'key':1, 'key2':2}, {'key':3, 'key2':4}])
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):
expected = {
'int': '20 is greater than maximum value 10',
'ob': 'The dotted name "no.way.this.exists" cannot be imported',
'seq.0.0': '"q" is not a number',
'seq.1.0': '"w" is not a number',
'seq.2.0': '"e" is not a number',
'seq.3.0': '"r" is not a number',
'seq2.0.key': '"t" is not a number',
'seq2.0.key2': '"y" is not a number',
'seq2.1.key': '"u" is not a number',
'seq2.1.key2': '"i" is not a number',
'tup.0': '"s" is not a number'}
'schema.int': '20 is greater than maximum value 10',
'schema.ob': 'The dotted name "no.way.this.exists" cannot be imported',
'schema.seq.0.0': '"q" is not a number',
'schema.seq.1.0': '"w" is not a number',
'schema.seq.2.0': '"e" is not a number',
'schema.seq.3.0': '"r" is not a number',
'schema.seq2.0.key': '"t" is not a number',
'schema.seq2.0.key2': '"y" is not a number',
'schema.seq2.1.key': '"u" is not a number',
'schema.seq2.1.key2': '"i" is not a number',
'schema.tup.0': '"s" is not a number'}
data = {
'int':'20',
'ob':'no.way.this.exists',
@@ -1701,7 +1808,8 @@ class TestImperative(unittest.TestCase, TestFunctional):
ob,
tup,
seq,
seq2)
seq2,
name='schema')
return schema
@@ -1721,7 +1829,7 @@ class TestDeclarative(unittest.TestCase, TestFunctional):
key2 = colander.SchemaNode(colander.Int())
class SequenceOne(colander.SequenceSchema):
tuple = TupleSchema()
tup = TupleSchema()
class SequenceTwo(colander.SequenceSchema):
mapping = MappingSchema()
@@ -1734,7 +1842,7 @@ class TestDeclarative(unittest.TestCase, TestFunctional):
tup = TupleSchema()
seq2 = SequenceTwo()
schema = MainSchema()
schema = MainSchema(name='schema')
return schema
class Test_null(unittest.TestCase):
@@ -1792,3 +1900,7 @@ class DummyType(object):
def deserialize(self, node, value):
return value
def flatten(self, node, appstruct, prefix=''):
key = prefix + 'appstruct'
return {key:appstruct}