Commits

Andy Mikhailenko committed 53aa9bd

Refactored and added documentation.

  • Participants
  • Parent commits 0c032a5

Comments (0)

Files changed (5)

File monk.py

-# -*- coding: utf-8 -*-
-#
-#    Monk is a lightweight schema/query framework for document databases.
-#    Copyright © 2011  Andrey Mikhaylenko
-#
-#    This file is part of Monk.
-#
-#    Monk is free software: you can redistribute it and/or modify
-#    it under the terms of the GNU Lesser General Public License as published
-#    by the Free Software Foundation, either version 3 of the License, or
-#    (at your option) any later version.
-#
-#    Monk is distributed in the hope that it will be useful,
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#    GNU Lesser General Public License for more details.
-#
-#    You should have received a copy of the GNU Lesser General Public License
-#    along with Monk.  If not, see <http://gnu.org/licenses/>.
-"""
-Monk
-====
-
-A simple schema validation layer for pymongo_. Inspired by MongoKit and Doqu.
-
-.. _pymongo: http://api.mongodb.org/python/current/
-
-"""
-from collections import deque
-from functools import partial
-
-from pymongo import dbref
-
-
-class DotExpandedDictMixin(object):
-    def __getattr__(self, attr):
-        if not attr.startswith('_') and attr in self:
-            return self[attr]
-        raise AttributeError('Attribute or key {0.__class__.__name__}.{1} '
-                             'does not exist'.format(self, attr))
-
-
-class TypedDictReprMixin(object):
-    def __repr__(self):
-        return '<{0.__class__.__name__} {1}>'.format(self, unicode(self))
-
-    def __unicode__(self):
-        return unicode(dict(self))
-
-
-class MongoResultSet(object):
-    def __init__(self, cursor, wrapper):
-        self._cursor = cursor
-        self._wrap = wrapper
-
-    def __iter__(self):
-        return (self._wrap(x) for x in self._cursor)
-
-    def __getattr__(self, attr):
-        return getattr(self._cursor, attr)
-
-
-class MongoBoundDictMixin(object):
-    collection = None
-    indexes = []
-
-    def __hash__(self):
-        "Collection name and id together make the hash."
-        return hash(self.collection) | hash(self.get('_id'))
-
-    @classmethod
-    def _ensure_indexes(cls, db):
-        for definition in cls.indexes:
-            fields = definition['fields']
-            for field in fields:
-                kwargs = dict(definition)
-                kwargs.pop('fields')
-                db[cls.collection].ensure_index(field, **kwargs)
-
-    @classmethod
-    def wrap_incoming(cls, data, db):
-        return cls(dict_from_db(data, db))
-
-    @classmethod
-    def find(cls, db, *args, **kwargs):
-        cls._ensure_indexes(db)
-        docs = db[cls.collection].find(*args, **kwargs)
-        return MongoResultSet(docs, partial(cls.wrap_incoming, db=db))
-
-    @classmethod
-    def get_one(cls, db, *args, **kwargs):
-        cls._ensure_indexes(db)
-        data = db[cls.collection].find_one(*args, **kwargs)
-        if data:
-            return cls.wrap_incoming(data, db)
-
-    def save(self, db):
-        assert self.collection
-        #self.populate_defaults()
-        self.validate()
-        outgoing = dict(dict_to_db(self, self.structure))
-        db[self.collection].save(outgoing)
-
-
-class StructuredDict(dict):
-    """ Словарь с валидацией структуры.
-    """
-    structure = {}
-    defaults = {}
-    #required = []
-    #validators = {}
-    with_skeleton = True
-
-#    def __init__(self, *args, **kwargs):
-#        super(StructuredDict, self).__init__(*args, **kwargs)
-
-    def validate(self):
-        # TODO
-        pass
-
-
-class Document(TypedDictReprMixin, MongoBoundDictMixin, DotExpandedDictMixin,
-               StructuredDict):
-    """ Структурированный словарь с привязкой с MongoDB и getattr->getitem.
-    """
-
-
-def dict_from_db(data, db):
-    def generate():
-        for key, value in data.iteritems():
-            if isinstance(value, dbref.DBRef):
-                yield key, dict(
-                    db.dereference(value),
-                    _id = value['_id']
-                )
-            else:
-                yield key, value
-    return dict(generate())
-
-
-def dict_to_db(data, structure={}):
-    def generate():
-        for key, value in data.iteritems():
-            if hasattr(value, '_id'):
-                collection = structure[key].collection
-                yield key, dbref.DBRef(collection, value['_id'])
-            else:
-                yield key, value
-    return dict(generate())
-
-
-
-class StructureSpecificationError(Exception):
-    pass
-
-
-def get_item(dictionary, keys, transparent_lists=False):
-    """ Usage::
-
-        >>> data = {'foo': {'bar': {'baz': 123}}}
-
-        # drill down the dictionary
-        >>> get_item(data, ('foo','bar','baz'))
-        123
-        >>> get_item(data, ('quux',))
-        Traceback (most recent call last):
-          ...
-        KeyError: 'quux'
-
-        # same as:
-        >>> data['foo']['bar']['baz']
-        123
-
-    """
-    value = dictionary
-    for key in keys:
-        value = value[key]
-    return value
-
-
-def walk_dict(data):
-    """ Usage::
-
-        >>> data = {
-        ...     'foo': {
-        ...         'bar': 123,
-        ...         'baz': {
-        ...             {'quux': 456},
-        ...         }
-        ...     },
-        ...     'yada': u'yadda'
-        ... }
-        ...
-        >>> for keys, value in walk_dict(data):
-        ...     print keys, value
-        ...
-        ('foo',)
-        ('foo', 'bar') 123
-        ('foo', 'baz')
-        ('foo', 'baz', 'quux') 456
-        ('yada',) yadda
-
-    """
-    for key, value in data.iteritems():
-        if isinstance(value, dict):
-            yield (key,), None
-            for keys, value in walk_dict(value):
-                path = (key,) + keys
-                yield path, value
-        else:
-            yield (key,), value
-
-
-def validate_structure_spec(spec):
-    """ Checks whether given document structure specification dictionary if
-    defined correctly.
-    Raises `StructureSpecificationError` if the specification is malformed.
-    """
-    stack = deque(walk_dict(spec))
-    while stack:
-        keys, value = stack.pop()
-        if isinstance(value, list):
-            # accepted: list of values of given type
-            # e.g.: [unicode] -> [u'foo', u'bar']
-            if len(value) == 1:
-                stack.append((keys, value[0]))
-            else:
-                raise StructureSpecificationError(
-                    '{path}: list must contain exactly 1 item (got {count})'
-                         .format(path='.'.join(keys), count=len(value)))
-        elif isinstance(value, dict):
-            # accepted: nested dictionary (a spec on its own)
-            # e.g.: {...} -> {...}
-            for subkeys, subvalue in walk_dict(value):
-                stack.append((keys+subkeys, subvalue))
-        elif value is None:
-            # accepted: any value
-            # e.g.: None -> 123
-            pass
-        elif isinstance(value, type):
-            # accepted: given type
-            # e.g.: unicode -> u'foo'   or   dict -> {'a': 123}   or whatever.
-            pass
-        else:
-            raise StructureSpecificationError(
-                '{path}: expected dict, list, type or None (got {value!r})'
-                    .format(path='.'.join(keys), value=value))
-
-
-def check_type(typespec, value, keys_tuple):
-    if typespec is None:
-        return
-    if not isinstance(typespec, type):
-        key = '.'.join(keys_tuple)
-        raise StructureSpecificationError(
-            '{path}: expected dict, list, type or None (got {value!r})'
-                .format(path=key, value=value))
-    if not isinstance(value, typespec):
-        key = '.'.join(keys_tuple)
-        raise TypeError('{key}: expected {typespec.__name__}, got '
-                        '{valtype.__name__} {value!r}'.format(key=key,
-                        typespec=typespec, valtype=type(value), value=value))
-
-
-def validate_structure(spec, data, skip_missing=False, skip_unknown=False):
-    """ Validates given document against given structure specification.
-    """
-    # flatten the structures so that nested dictionaries are moved to the root
-    # level and {'a': {'b': 1}} becomes {('a','b'): 1}
-    flat_spec = dict(walk_dict(spec))
-    flat_data = dict(walk_dict(data))
-
-    # compare the two structures; nested dictionaries are included in the
-    # comparison but nested lists are opaque and will be dealt with later on.
-    spec_keys = set(spec.iterkeys())
-    data_keys = set(data.iterkeys())
-    missing = spec_keys - data_keys
-    unknown = data_keys - spec_keys
-
-    if missing and not skip_missing:
-        raise KeyError('Missing keys: {0}'.format(', '.join(missing)))
-
-    if unknown and not skip_unknown:
-        raise KeyError('Unknown keys: {0}'.format(', '.join(unknown)))
-
-    # check types and deal with nested lists
-    for keys, value in flat_data.iteritems():
-        typespec = flat_spec.get(keys)
-        if value is None:
-            # empty value, ok unless required
-            continue
-        elif typespec is None:
-            # any value is acceptable
-            continue
-        elif isinstance(typespec, list) and value:
-            # nested list
-            item_spec = typespec[0]
-            for item in value:
-                if item_spec == dict or isinstance(item, dict):
-                    # validate each value in the list as a separate document
-                    # and fix error message to include outer key
-                    try:
-                        validate_structure(item_spec, item,
-                                           skip_missing=skip_missing,
-                                           skip_unknown=skip_unknown)
-                    except (KeyError, TypeError) as e:
-                        raise type(e)('{k}: {e}'.format(k='.'.join(keys), e=e))
-                else:
-                    check_type(item_spec, item, keys)
-        else:
-            check_type(typespec, value, keys)

File monk/__init__.py

+# -*- coding: utf-8 -*-
+#
+#    Monk is a lightweight schema/query framework for document databases.
+#    Copyright © 2011  Andrey Mikhaylenko
+#
+#    This file is part of Monk.
+#
+#    Monk is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Lesser General Public License as published
+#    by the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    Monk is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Lesser General Public License for more details.
+#
+#    You should have received a copy of the GNU Lesser General Public License
+#    along with Monk.  If not, see <http://gnu.org/licenses/>.
+"""
+Monk
+====
+
+A simple schema validation layer for pymongo_. Inspired by MongoKit and Doqu.
+
+.. _pymongo: http://api.mongodb.org/python/current/
+
+"""

File monk/models.py

+# -*- coding: utf-8 -*-
+#
+#    Monk is a lightweight schema/query framework for document databases.
+#    Copyright © 2011  Andrey Mikhaylenko
+#
+#    This file is part of Monk.
+#
+#    Monk is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Lesser General Public License as published
+#    by the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    Monk is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Lesser General Public License for more details.
+#
+#    You should have received a copy of the GNU Lesser General Public License
+#    along with Monk.  If not, see <http://gnu.org/licenses/>.
+"""
+Models
+======
+"""
+from functools import partial
+
+from pymongo import dbref
+
+from monk import validation
+
+
+class DotExpandedDictMixin(object):
+    def __getattr__(self, attr):
+        if not attr.startswith('_') and attr in self:
+            return self[attr]
+        raise AttributeError('Attribute or key {0.__class__.__name__}.{1} '
+                             'does not exist'.format(self, attr))
+
+
+class TypedDictReprMixin(object):
+    def __repr__(self):
+        return '<{0.__class__.__name__} {1}>'.format(self, unicode(self))
+
+    def __unicode__(self):
+        return unicode(dict(self))
+
+
+class MongoResultSet(object):
+    def __init__(self, cursor, wrapper):
+        self._cursor = cursor
+        self._wrap = wrapper
+
+    def __iter__(self):
+        return (self._wrap(x) for x in self._cursor)
+
+    def __getattr__(self, attr):
+        return getattr(self._cursor, attr)
+
+
+class MongoBoundDictMixin(object):
+    collection = None
+    indexes = []
+
+    def __hash__(self):
+        "Collection name and id together make the hash."
+        return hash(self.collection) | hash(self.get('_id'))
+
+    @classmethod
+    def _ensure_indexes(cls, db):
+        for definition in cls.indexes:
+            fields = definition['fields']
+            for field in fields:
+                kwargs = dict(definition)
+                kwargs.pop('fields')
+                db[cls.collection].ensure_index(field, **kwargs)
+
+    @classmethod
+    def wrap_incoming(cls, data, db):
+        return cls(dict_from_db(data, db))
+
+    @classmethod
+    def find(cls, db, *args, **kwargs):
+        cls._ensure_indexes(db)
+        docs = db[cls.collection].find(*args, **kwargs)
+        return MongoResultSet(docs, partial(cls.wrap_incoming, db=db))
+
+    @classmethod
+    def get_one(cls, db, *args, **kwargs):
+        cls._ensure_indexes(db)
+        data = db[cls.collection].find_one(*args, **kwargs)
+        if data:
+            return cls.wrap_incoming(data, db)
+
+    def save(self, db):
+        assert self.collection
+        #self.populate_defaults()
+        self.validate()
+        outgoing = dict(dict_to_db(self, self.structure))
+        db[self.collection].save(outgoing)
+
+
+class StructuredDict(dict):
+    """ A dictionary with structure specification and validation.
+    """
+    structure = {}
+    defaults = {}
+    #required = []
+    #validators = {}
+    with_skeleton = True
+
+#    def __init__(self, *args, **kwargs):
+#        super(StructuredDict, self).__init__(*args, **kwargs)
+
+    def validate(self):
+        validation.validate_structure(self.structure, self)
+
+
+class Document(TypedDictReprMixin, MongoBoundDictMixin, DotExpandedDictMixin,
+               StructuredDict):
+    """ A structured dictionary that is bound to MongoDB and supports dot
+    notation for access to items.
+    """
+
+
+def dict_from_db(data, db):
+    def generate():
+        for key, value in data.iteritems():
+            if isinstance(value, dbref.DBRef):
+                yield key, dict(
+                    db.dereference(value),
+                    _id = value['_id']
+                )
+            else:
+                yield key, value
+    return dict(generate())
+
+
+def dict_to_db(data, structure={}):
+    def generate():
+        for key, value in data.iteritems():
+            if hasattr(value, '_id'):
+                collection = structure[key].collection
+                yield key, dbref.DBRef(collection, value['_id'])
+            else:
+                yield key, value
+    return dict(generate())

File monk/validation.py

+# -*- coding: utf-8 -*-
+#
+#    Monk is a lightweight schema/query framework for document databases.
+#    Copyright © 2011  Andrey Mikhaylenko
+#
+#    This file is part of Monk.
+#
+#    Monk is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Lesser General Public License as published
+#    by the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    Monk is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Lesser General Public License for more details.
+#
+#    You should have received a copy of the GNU Lesser General Public License
+#    along with Monk.  If not, see <http://gnu.org/licenses/>.
+"""
+Validation
+==========
+"""
+from collections import deque
+
+
+class StructureSpecificationError(Exception):
+    "Raised when malformed document structure is detected."
+    pass
+
+
+class MissingKey(KeyError):
+    """ Raised when a key is defined in the structure spec but is missing from
+    a data dictionary.
+    """
+
+
+class UnknownKey(KeyError):
+    """ Raised when a key in data dictionary is missing from the corresponding
+    structure spec.
+    """
+
+
+def walk_dict(data):
+    """ Generates pairs ``(keys, value)`` for each item in given dictionary,
+    including nested dictionaries. Each pair contains:
+
+    `keys`
+        a tuple of 1..n keys, e.g. ``('foo',)`` for a key on root level or
+        ``('foo', 'bar')`` for a key in a nested dictionary.
+    `value`
+        the value of given key or ``None`` if it is a nested dictionary and
+        therefore can be further unwrapped.
+    """
+    assert hasattr(data, '__getitem__')
+    for key, value in data.iteritems():
+        if isinstance(value, dict):
+            yield (key,), None
+            for keys, value in walk_dict(value):
+                path = (key,) + keys
+                yield path, value
+        else:
+            yield (key,), value
+
+
+def validate_structure_spec(spec):
+    """ Checks whether given document structure specification dictionary if
+    defined correctly.
+    Raises `StructureSpecificationError` if the specification is malformed.
+    """
+    stack = deque(walk_dict(spec))
+    while stack:
+        keys, value = stack.pop()
+        if isinstance(value, list):
+            # accepted: list of values of given type
+            # e.g.: [unicode] -> [u'foo', u'bar']
+            if len(value) == 1:
+                stack.append((keys, value[0]))
+            else:
+                raise StructureSpecificationError(
+                    '{path}: list must contain exactly 1 item (got {count})'
+                         .format(path='.'.join(keys), count=len(value)))
+        elif isinstance(value, dict):
+            # accepted: nested dictionary (a spec on its own)
+            # e.g.: {...} -> {...}
+            for subkeys, subvalue in walk_dict(value):
+                stack.append((keys+subkeys, subvalue))
+        elif value is None:
+            # accepted: any value
+            # e.g.: None -> 123
+            pass
+        elif isinstance(value, type):
+            # accepted: given type
+            # e.g.: unicode -> u'foo'   or   dict -> {'a': 123}   or whatever.
+            pass
+        else:
+            raise StructureSpecificationError(
+                '{path}: expected dict, list, type or None (got {value!r})'
+                    .format(path='.'.join(keys), value=value))
+
+
+def check_type(typespec, value, keys_tuple):
+    if typespec is None:
+        return
+    if not isinstance(typespec, type):
+        key = '.'.join(keys_tuple)
+        raise StructureSpecificationError(
+            '{path}: expected dict, list, type or None (got {value!r})'
+                .format(path=key, value=value))
+    if not isinstance(value, typespec):
+        key = '.'.join(keys_tuple)
+        raise TypeError('{key}: expected {typespec.__name__}, got '
+                        '{valtype.__name__} {value!r}'.format(key=key,
+                        typespec=typespec, valtype=type(value), value=value))
+
+
+def validate_structure(spec, data, skip_missing=False, skip_unknown=False):
+    """ Validates given document against given structure specification.
+
+    Always returns ``None``.
+
+    Can raise:
+
+    :class:`MissingKey`
+        if a key is in `spec` but not in `data`.
+    :class:`UnknownKey`
+        if a key is in `data` but not in `spec`.
+    :class:`StructureSpecificationError`
+        if errors were found in `spec`.
+    :class:`TypeError`
+        if a value in `data` does not belong to the designated type.
+
+    Arguments:
+
+    :param spec:
+        `dict`; document structure specification.
+    :param data:
+        `dict`; document to be validated against the spec.
+    :param skip_missing:
+        ``bool``; if ``True``, :class:`MissingKey` is never raised.
+        Default is ``False``.
+    :param skip_unknown:
+        ``bool``; if ``True``, :class:`UnknownKey` is never raised.
+        Default is ``False``.
+
+    """
+    # flatten the structures so that nested dictionaries are moved to the root
+    # level and {'a': {'b': 1}} becomes {('a','b'): 1}
+    flat_spec = dict(walk_dict(spec))
+    flat_data = dict(walk_dict(data))
+
+    # compare the two structures; nested dictionaries are included in the
+    # comparison but nested lists are opaque and will be dealt with later on.
+    spec_keys = set(spec.iterkeys())
+    data_keys = set(data.iterkeys())
+    missing = spec_keys - data_keys
+    unknown = data_keys - spec_keys
+
+    if missing and not skip_missing:
+        raise MissingKey('Missing keys: {0}'.format(', '.join(missing)))
+
+    if unknown and not skip_unknown:
+        raise UnknownKey('Unknown keys: {0}'.format(', '.join(unknown)))
+
+    # check types and deal with nested lists
+    for keys, value in flat_data.iteritems():
+        typespec = flat_spec.get(keys)
+        if value is None:
+            # empty value, ok unless required
+            continue
+        elif typespec is None:
+            # any value is acceptable
+            continue
+        elif isinstance(typespec, list) and value:
+            # nested list
+            item_spec = typespec[0]
+            for item in value:
+                if item_spec == dict or isinstance(item, dict):
+                    # validate each value in the list as a separate document
+                    # and fix error message to include outer key
+                    try:
+                        validate_structure(item_spec, item,
+                                           skip_missing=skip_missing,
+                                           skip_unknown=skip_unknown)
+                    except (MissingKey, UnknownKey, TypeError) as e:
+                        raise type(e)('{k}: {e}'.format(k='.'.join(keys), e=e))
+                else:
+                    check_type(item_spec, item, keys)
+        else:
+            check_type(typespec, value, keys)
 import pymongo.objectid
 import unittest2
 
-import monk
+from monk.validation import (
+    walk_dict, validate_structure_spec, validate_structure,
+    StructureSpecificationError
+)
+from monk import models
 
 
 class StructureSpecTestCase(unittest2.TestCase):
             (('h',), list),
             (('i',), None),
         ]
-        self.assertEqual(sorted(monk.walk_dict(data)), sorted(paths))
+        self.assertEqual(sorted(walk_dict(data)), sorted(paths))
 
     def test_correct_types(self):
         """ `None` stands for "any value". """
-        monk.validate_structure_spec({'foo': None})
-        monk.validate_structure_spec({'foo': bool})
-        monk.validate_structure_spec({'foo': dict})
-        monk.validate_structure_spec({'foo': float})
-        monk.validate_structure_spec({'foo': int})
-        monk.validate_structure_spec({'foo': list})
-        monk.validate_structure_spec({'foo': unicode})
-        monk.validate_structure_spec({'foo': datetime.datetime})
-        monk.validate_structure_spec({'foo': pymongo.binary.Binary})
-        monk.validate_structure_spec({'foo': pymongo.code.Code})
-        monk.validate_structure_spec({'foo': pymongo.objectid.ObjectId})
-        monk.validate_structure_spec({'foo': pymongo.dbref.DBRef})
+        validate_structure_spec({'foo': None})
+        validate_structure_spec({'foo': bool})
+        validate_structure_spec({'foo': dict})
+        validate_structure_spec({'foo': float})
+        validate_structure_spec({'foo': int})
+        validate_structure_spec({'foo': list})
+        validate_structure_spec({'foo': unicode})
+        validate_structure_spec({'foo': datetime.datetime})
+        validate_structure_spec({'foo': pymongo.binary.Binary})
+        validate_structure_spec({'foo': pymongo.code.Code})
+        validate_structure_spec({'foo': pymongo.objectid.ObjectId})
+        validate_structure_spec({'foo': pymongo.dbref.DBRef})
 
     def test_correct_structures(self):
         # foo is of given type
-        monk.validate_structure_spec({'foo': int})
+        validate_structure_spec({'foo': int})
         # foo and bar are of given types
-        monk.validate_structure_spec({'foo': int, 'bar': unicode})
+        validate_structure_spec({'foo': int, 'bar': unicode})
         # foo is a list of values of given type
-        monk.validate_structure_spec({'foo': [int]})
+        validate_structure_spec({'foo': [int]})
         # foo.bar is of given type
-        monk.validate_structure_spec({'foo': {'bar': int}})
+        validate_structure_spec({'foo': {'bar': int}})
         # foo.bar is a list of values of given type
-        monk.validate_structure_spec({'foo': {'bar': [int]}})
+        validate_structure_spec({'foo': {'bar': [int]}})
         # foo.bar is a list of mappings where each "baz" is of given type
-        monk.validate_structure_spec({'foo': {'bar': [{'baz': [unicode]}]}})
+        validate_structure_spec({'foo': {'bar': [{'baz': [unicode]}]}})
 
     def test_bad_types(self):
         # instances are not accepted; only types
-        with self.assertRaisesRegexp(monk.StructureSpecificationError, 'type'):
-            monk.validate_structure_spec({'foo': u'hello'})
+        with self.assertRaisesRegexp(StructureSpecificationError, 'type'):
+            validate_structure_spec({'foo': u'hello'})
 
-        with self.assertRaisesRegexp(monk.StructureSpecificationError, 'type'):
-            monk.validate_structure_spec({'foo': 123})
+        with self.assertRaisesRegexp(StructureSpecificationError, 'type'):
+            validate_structure_spec({'foo': 123})
 
     def test_malformed_lists(self):
         single_elem_err_msg = 'list must contain exactly 1 item'
 
-        with self.assertRaisesRegexp(monk.StructureSpecificationError, single_elem_err_msg):
-            monk.validate_structure_spec({'foo': []})
+        with self.assertRaisesRegexp(StructureSpecificationError, single_elem_err_msg):
+            validate_structure_spec({'foo': []})
 
-        with self.assertRaisesRegexp(monk.StructureSpecificationError, single_elem_err_msg):
-            monk.validate_structure_spec({'foo': [unicode, unicode]})
+        with self.assertRaisesRegexp(StructureSpecificationError, single_elem_err_msg):
+            validate_structure_spec({'foo': [unicode, unicode]})
 
-        with self.assertRaisesRegexp(monk.StructureSpecificationError, single_elem_err_msg):
-            monk.validate_structure_spec({'foo': {'bar': [unicode, unicode]}})
+        with self.assertRaisesRegexp(StructureSpecificationError, single_elem_err_msg):
+            validate_structure_spec({'foo': {'bar': [unicode, unicode]}})
 
-        with self.assertRaisesRegexp(monk.StructureSpecificationError, single_elem_err_msg):
-            monk.validate_structure_spec({'foo': {'bar': [{'baz': [unicode, unicode]}]}})
+        with self.assertRaisesRegexp(StructureSpecificationError, single_elem_err_msg):
+            validate_structure_spec({'foo': {'bar': [{'baz': [unicode, unicode]}]}})
 
 
 class DocumentStructureValidationTestCase(unittest2.TestCase):
 
     def test_empty(self):
-        monk.validate_structure({'a': unicode}, {'a': None})
-        monk.validate_structure({'a': list}, {'a': None})
-        monk.validate_structure({'a': dict}, {'a': None})
+        validate_structure({'a': unicode}, {'a': None})
+        validate_structure({'a': list}, {'a': None})
+        validate_structure({'a': dict}, {'a': None})
 
         # None is allowed to represent empty value, but bool(value)==False
         # is not (unless bool is the correct type for this value)
-        monk.validate_structure({'a': bool}, {'a': None})
-        monk.validate_structure({'a': bool}, {'a': False})
+        validate_structure({'a': bool}, {'a': None})
+        validate_structure({'a': bool}, {'a': False})
         with self.assertRaises(TypeError):
-            monk.validate_structure({'a': unicode}, {'a': False})
+            validate_structure({'a': unicode}, {'a': False})
         with self.assertRaises(TypeError):
-            monk.validate_structure({'a': unicode}, {'a': 0})
+            validate_structure({'a': unicode}, {'a': 0})
         with self.assertRaises(TypeError):
-            monk.validate_structure({'a': bool}, {'a': u''})
+            validate_structure({'a': bool}, {'a': u''})
 
     def test_missing(self):
-        monk.validate_structure({'a': unicode}, {}, skip_missing=True)
+        validate_structure({'a': unicode}, {}, skip_missing=True)
         with self.assertRaises(KeyError):
-            monk.validate_structure({'a': unicode}, {})
+            validate_structure({'a': unicode}, {})
         with self.assertRaises(KeyError):
-            monk.validate_structure({'a': unicode, 'b': int}, {'b': 1})
+            validate_structure({'a': unicode, 'b': int}, {'b': 1})
 
     def test_unknown_keys(self):
-        monk.validate_structure({}, {'x': 123}, skip_unknown=True)
+        validate_structure({}, {'x': 123}, skip_unknown=True)
         with self.assertRaises(KeyError):
-            monk.validate_structure({}, {'x': 123})
+            validate_structure({}, {'x': 123})
         with self.assertRaises(KeyError):
-            monk.validate_structure({'a': unicode}, {'a': u'A', 'x': 123})
+            validate_structure({'a': unicode}, {'a': u'A', 'x': 123})
         with self.assertRaisesRegexp(TypeError, "a: b: expected int, got str 'bad'"):
-            monk.validate_structure({'a': [{'b': [int]}]}, {'a': [{'b': ['bad']}]})
+            validate_structure({'a': [{'b': [int]}]}, {'a': [{'b': ['bad']}]})
 
     def test_bool(self):
-        monk.validate_structure({'a': bool}, {'a': None})
-        monk.validate_structure({'a': bool}, {'a': True})
-        monk.validate_structure({'a': bool}, {'a': False})
+        validate_structure({'a': bool}, {'a': None})
+        validate_structure({'a': bool}, {'a': True})
+        validate_structure({'a': bool}, {'a': False})
 
     def test_dict(self):
-        monk.validate_structure({'a': dict}, {'a': None})
-        monk.validate_structure({'a': dict}, {'a': {}})
-        monk.validate_structure({'a': dict}, {'a': {'b': 'c'}})
+        validate_structure({'a': dict}, {'a': None})
+        validate_structure({'a': dict}, {'a': {}})
+        validate_structure({'a': dict}, {'a': {'b': 'c'}})
 
     def test_float(self):
-        monk.validate_structure({'a': float}, {'a': None})
-        monk.validate_structure({'a': float}, {'a': .5})
+        validate_structure({'a': float}, {'a': None})
+        validate_structure({'a': float}, {'a': .5})
 
     def test_int(self):
-        monk.validate_structure({'a': int}, {'a': None})
-        monk.validate_structure({'a': int}, {'a': 123})
+        validate_structure({'a': int}, {'a': None})
+        validate_structure({'a': int}, {'a': 123})
 
     def test_list(self):
-        monk.validate_structure({'a': list}, {'a': None})
-        monk.validate_structure({'a': list}, {'a': []})
-        monk.validate_structure({'a': list}, {'a': ['b', 123]})
+        validate_structure({'a': list}, {'a': None})
+        validate_structure({'a': list}, {'a': []})
+        validate_structure({'a': list}, {'a': ['b', 123]})
 
     def test_unicode(self):
-        monk.validate_structure({'a': unicode}, {'a': None})
-        monk.validate_structure({'a': unicode}, {'a': u'hello'})
+        validate_structure({'a': unicode}, {'a': None})
+        validate_structure({'a': unicode}, {'a': u'hello'})
         with self.assertRaises(TypeError):
-            monk.validate_structure({'a': unicode}, {'a': 123})
+            validate_structure({'a': unicode}, {'a': 123})
 
     def test_datetime(self):
-        monk.validate_structure({'a': datetime.datetime}, {'a': None})
-        monk.validate_structure({'a': datetime.datetime},
+        validate_structure({'a': datetime.datetime}, {'a': None})
+        validate_structure({'a': datetime.datetime},
                                 {'a': datetime.datetime.utcnow()})
 
     def test_objectid(self):
-        monk.validate_structure({'a': pymongo.objectid.ObjectId}, {'a': None})
-        monk.validate_structure({'a': pymongo.objectid.ObjectId},
+        validate_structure({'a': pymongo.objectid.ObjectId}, {'a': None})
+        validate_structure({'a': pymongo.objectid.ObjectId},
                                 {'a': pymongo.objectid.ObjectId()})
 
     def test_dbref(self):
-        monk.validate_structure({'a': pymongo.dbref.DBRef}, {'a': None})
-        monk.validate_structure({'a': pymongo.dbref.DBRef},
+        validate_structure({'a': pymongo.dbref.DBRef}, {'a': None})
+        validate_structure({'a': pymongo.dbref.DBRef},
                                 {'a': pymongo.dbref.DBRef('a', 'b')})
 
     def test_valid_document(self):
                 },
             ],
         }
-        monk.validate_structure(spec, data)
+        validate_structure(spec, data)
+
+
+
+class DocumentDefaultsTestCase(unittest2.TestCase):
+    class Entry(models.Document):
+        structure = {
+            'title': unicode,
+            'author': {
+                'first_name': unicode,
+                'last_name': unicode,
+            },
+            'comments': [
+                {
+                    'text': unicode,
+                    'is_spam': bool,
+                },
+            ]
+        }
+        defaults = {
+            'comments.is_spam': False,
+        }
+    data = {
+        'title': u'Hello',
+        'author': {
+            'first_name': u'John',
+            'last_name': u'Doe',
+        },
+        'comments': [
+            # XXX when do we add the default value is_spam=False?
+            # anything that is inside a list (0..n) cannot be included in skel.
+            # (just check or also append defaults) on (add / save / validate)?
+            {'text': u'Oh hi'},
+            {'text': u'Hi there', 'is_spam': True},
+        ]
+    }
+    def test_basic_document(self):
+        entry = self.Entry(self.data)
+        self.assertEquals(entry['title'], self.data['title'])
+        with self.assertRaises(KeyError):
+            entry['nonexistent_key']
+
+    @unittest2.expectedFailure
+    def test_dot_expanded(self):
+        entry = self.Entry(self.data)
+        self.assertEquals(entry.title, entry['title'])
+        with self.assertRaises(AttributeError):
+            entry.nonexistent_key
+        self.assertEquals(entry.author.first_name,
+                          entry['author']['first_name'])