Commits

Andy Mikhailenko  committed a3a89a4

Move list inner_spec conversion to canonize() and simplify it. Move error declarations to monk.errors.

  • Participants
  • Parent commits a448358
  • Branches layered-specs

Comments (0)

Files changed (5)

File monk/errors.py

+# coding: utf-8
+#
+#    Monk is an unobtrusive data modeling, manipulation and validation library.
+#    Copyright © 2011—2013  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/>.
+"""
+Exceptions
+~~~~~~~~~~
+"""
+class ValidationError(Exception):
+    "Raised when a document or its part cannot pass validation."
+
+
+class StructureSpecificationError(ValidationError):
+    "Raised when malformed document structure is detected."
+
+
+class MissingValue(ValidationError):
+    """ Raised when the value is `None` and the rule neither allows this
+    (i.e. a `datatype` is defined) nor provides a `default` value.
+    """
+
+
+class MissingKey(ValidationError):
+    """ Raised when a dictionary key is defined in :attr:`Rule.inner_spec`
+    but is missing from the value.
+    """
+
+
+class UnknownKey(ValidationError):
+    """ Raised whan the value dictionary contains a key which is not
+    in the dictionary's :attr:`Rule.inner_spec`.
+    """

File monk/schema.py

 Schema Definition
 ~~~~~~~~~~~~~~~~~
 """
-from . import compat
+from . import compat, errors
 
 
 __all__ = [
             raise TypeError('Default value must match datatype {0} (got {1})'.format(
                 self.datatype.__name__, default))
 
-        if self.inner_spec and not isinstance(self.inner_spec, self.datatype):
-            raise TypeError('Inner spec must match datatype {0} (got {1})'.format(
-                self.datatype.__name__, inner_spec))
+#        if self.inner_spec and not isinstance(self.inner_spec, self.datatype):
+#            raise TypeError('Inner spec must match datatype {0} (got {1})'.format(
+#                self.datatype.__name__, inner_spec))
 
     def __repr__(self):
         return '<Rule {datatype}{optional}{default}{inner_spec}{skip_unknown_keys}>'.format(
     elif type(value) in compat.func_types:
         real_value = value()
         kwargs = dict(rule_kwargs, default=real_value)
-        rule = Rule(type(real_value), **kwargs)
-    elif type(value) in (dict, list):
+        rule = Rule(datatype=type(real_value), **kwargs)
+    elif isinstance(value, list):
+        if value == []:
+            # no inner spec, just an empty list as the default value
+            kwargs = dict(rule_kwargs, default=value)
+            rule = Rule(datatype=list, **kwargs)
+        elif len(value) == 1:
+            # the only item as spec for each item of the collection
+            kwargs = dict(rule_kwargs, inner_spec=value[0])
+            rule = Rule(datatype=list, **kwargs)
+        else:
+            raise errors.StructureSpecificationError(
+                'Expected a list containing exactly 1 item; '
+                'got {cnt}: {spec}'.format(cnt=len(value), spec=value))
+    elif isinstance(value, dict):
         kwargs = dict(rule_kwargs, inner_spec=value)
-        rule = Rule(type(value), **kwargs)
+        rule = Rule(datatype=dict, **kwargs)
     else:
         kwargs = dict(rule_kwargs, default=value)
-        rule = Rule(type(value), **kwargs)
+        rule = Rule(datatype=type(value), **kwargs)
 
     return rule
 

File monk/validation.py

 """
 from . import compat
 from .schema import canonize
-
+from . import errors
 
 __all__ = [
-    # errors
-    'ValidationError', 'StructureSpecificationError', 'MissingKey',
-    'UnknownKey',
     # functions
     'validate'
 ]
 
 
-class ValidationError(Exception):
-    "Raised when a document or its part cannot pass validation."
-
-
-class StructureSpecificationError(ValidationError):
-    "Raised when malformed document structure is detected."
-
-
-class MissingValue(ValidationError):
-    """ Raised when the value is `None` and the rule neither allows this
-    (i.e. a `datatype` is defined) nor provides a `default` value.
-    """
-
-
-class MissingKey(ValidationError):
-    """ Raised when a dictionary key is defined in :attr:`Rule.inner_spec`
-    but is missing from the value.
-    """
-
-
-class UnknownKey(ValidationError):
-    """ Raised whan the value dictionary contains a key which is not
-    in the dictionary's :attr:`Rule.inner_spec`.
-    """
-
 
 def validate_dict(rule, value):
     """ Nested dictionary. May contain complex structures which are validated
     spec_keys = set(rule.inner_spec.keys() if rule.inner_spec else [])
     data_keys = set(value.keys() if value else [])
     unknown = data_keys - spec_keys
-    missing = spec_keys - data_keys
+    #missing = spec_keys - data_keys
 
     if unknown and not rule.dict_skip_unknown_keys:
-        raise UnknownKey('Unknown keys: {0}'.format(
+        raise errors.UnknownKey('Unknown keys: {0}'.format(
             ', '.join(compat.safe_str(x) for x in unknown)))
 
     for key in spec_keys | data_keys:
             value_ = value.get(key)
             try:
                 validate(subrule, value_)
-            except (ValidationError, TypeError) as e:
+            except (errors.ValidationError, TypeError) as e:
                 raise type(e)('{k}: {e}'.format(k=key, e=e))
         else:
             if subrule.optional:
                 continue
-            raise MissingKey('{0}'.format(key))
+            raise errors.MissingKey('{0}'.format(key))
 
 
 def validate_list(rule, value):
         # spec is [] which means "a list of anything"
         return
 
-    # FIXME this belongs to the internals of canonize()
-    #       and the "first item as spec for inner collection" thing
-    #       should go to a special Rule attribute
-    if 1 < len(rule.inner_spec):
-        raise StructureSpecificationError(
-            'Expected an empty list or a list containing exactly 1 item; '
-            'got {cnt}: {spec}'.format(cnt=len(rule.inner_spec), spec=rule.inner_spec))
-    item_spec = canonize(rule.inner_spec[0])
+    item_spec = canonize(rule.inner_spec)
+    assert item_spec
+    print('canonize', rule.inner_spec, '->', item_spec)
 
     # XXX custom validation stuff can be inserted here, e.g. min/max items
 
+    print('value:', value)
     for i, item in enumerate(value):
+        print('  item #', i,  'spec:', item_spec, 'item:', item)
         try:
             validate(item_spec, item)
-        except (ValidationError, TypeError) as e:
+        except (errors.ValidationError, TypeError) as e:
             raise type(e)('#{i}: {e}'.format(i=i, e=e))
 
 
             return
 
         if rule.datatype is None:
-            raise MissingValue('expected a value, got None')
+            raise errors.MissingValue('expected a value, got None')
         else:
-            raise MissingValue('expected {0}, got None'.format(rule.datatype.__name__))
+            raise errors.MissingValue('expected {0}, got None'.format(rule.datatype.__name__))
 
     if rule.datatype is None:
         # any value is acceptable

File unittests/test_rules.py

 import datetime
 import pytest
 
+from monk import errors
 from monk.schema import Rule, canonize
 
 
             Rule(str, default=123)
         assert excinfo.exconly() == 'TypeError: Default value must match datatype str (got 123)'
 
+    @pytest.mark.xfail
     def test_sanity_inner_spec(self):
+        #
+        # this won't work because only dict wants a dict as its inner_spec;
+        # a list doesn't need this duplication.
+        #
         Rule(dict)
         Rule(dict, inner_spec={})
         with pytest.raises(TypeError) as excinfo:
             Rule(dict, inner_spec=123)
         assert excinfo.exconly() == 'TypeError: Inner spec must match datatype dict (got 123)'
 
+
 class TestCanonization:
 
     def test_none(self):
 
     def test_list(self):
         assert canonize(list) == Rule(list)
-        assert canonize([1,2]) == Rule(list, inner_spec=[1,2])
+
+        assert canonize([]) == Rule(list, default=[])
+
+        with pytest.raises(errors.StructureSpecificationError) as excinfo:
+            canonize([1,2])
+        assert ("StructureSpecificationError: Expected a list "
+                "containing exactly 1 item; got 2: [1, 2]") in excinfo.exconly()
 
     def test_string(self):
         assert canonize(str) == Rule(str)

File unittests/test_validation.py

 import pytest
 
 from monk.compat import text_type, safe_unicode
+from monk.errors import MissingKey, MissingValue, UnknownKey
 from monk.schema import Rule, optional, any_value, any_or_none
-from monk.validation import (
-    validate, MissingValue, MissingKey, UnknownKey
-)
+from monk.validation import validate
 
 
 class TestOverall:
         validate(spec, {'foo': 123})
 
     def test_int_in_list(self):
-        spec = Rule(datatype=list, inner_spec=[int])
+        spec = Rule(datatype=list, inner_spec=int)
 
         # outer value is missing
 
         assert "TypeError: #1: expected int, got str 'bogus'" in excinfo.exconly()
 
     def test_freeform_dict_in_list(self):
-        spec = Rule(datatype=list, inner_spec=[dict])
+        spec = Rule(datatype=list, inner_spec=dict)
 
         # inner value is present
 
         assert "TypeError: #1: expected dict, got str 'bogus'" in excinfo.exconly()
 
     def test_schemed_dict_in_list(self):
-        spec = Rule(datatype=list, inner_spec=[{'foo': int}])
+        spec = Rule(datatype=list, inner_spec={'foo': int})
 
         # dict in list: missing key