Commits

Jean-Tiare Le Bigot committed 7ac07e3

add unit tests for _validate_dict + cleanup

  • Participants
  • Parent commits 314d162

Comments (0)

Files changed (2)

File tests/unit/test_schema.py

         m_validate.side_effect = Invalid(MSG1, PATH)
         self.assertRaises(Invalid, Schema(None)._validate_list, [], [1,2,3], [12])
 
+    @mock.patch('voluptuous.Schema.validate')
+    def test_validate_dict(self, m_validate):
+        from voluptuous import Schema, InvalidList, Invalid, Required, Optional, Extra
+
+        # test with empty schema and extra
+        self.assertEqual({'1':'2'}, Schema(None, extra=True)._validate_dict([], {}, {'1':'2'}))
+        self.assertRaisesRegexp(InvalidList, "extra", Schema(None)._validate_dict, [], {}, {'1':'2'})
+
+        # test per schema extra
+        self.assertEqual({'1':'2'}, Schema(None)._validate_dict([], {Extra: 'whatever'}, {'1':'2'}))
+
+        # no key required
+        self.assertEqual({}, Schema(None)._validate_dict([], {'1':'2'}, {}))
+
+        # all key required
+        m_validate.return_value = 42
+        self.assertEqual({42:42}, Schema(None, required=True)._validate_dict([], {'1':'2'}, {'1':'2'}))
+        self.assertRaisesRegexp(InvalidList, "required", Schema(None, required=True)._validate_dict, [], {'1':'2'}, {})
+
+        # all key required BUT optional (sic)
+        m_validate.return_value = 42
+        self.assertEqual({}, Schema(None, required=True)._validate_dict([], {Optional('1'):'2'}, {}))
+        self.assertEqual({42:42}, Schema(None, required=True)._validate_dict([], {Optional('1'):'2'}, {'1':'2'}))
+
+        # individual key required
+        m_validate.return_value = 42
+        self.assertEqual({42:42}, Schema(None)._validate_dict([], {Required('1'):'2'}, {'1':'2'}))
+        self.assertRaisesRegexp(InvalidList, "required", Schema(None)._validate_dict, [], {Required('1'):'2'}, {})

File voluptuous/voluptuous.py

 
         (This is to avoid unexpected surprises.)
         """
-        if not isinstance(data, dict):
-            raise Invalid('expected a dictionary', path)
+        # Empty schema when extra allowed, allow any data list.
+        if (not schema and self.extra):
+            return data # shortcut
 
         out = type(data)()
 
         required_keys = set()
         error = None
         errors = []
+        extra = self.extra
 
         # load required key list
-        for key in schema:
-            if isinstance(key, Required):
-                required_keys.append(key)
-            if self.required and not isinstance(key, optional):
-                required_keys.append(Required(key))
+        for skey in schema:
+            if (isinstance(skey, Required)
+            or self.required and not isinstance(skey, Optional)):
+                required_keys.add(skey)
 
         # loop over data to validate
         for key, value in data.iteritems():
             key_path = path + [key]
-            for skey, svalue in schema.iteritems():
+
+            # First, select a validator key by trying to match data key against it
+            for skey, rule in schema.iteritems():
                 if skey is Extra:
-                    new_key = key
+                    # "Extra" is a "match all". Use it in last resort
+                    extra = True
                 else:
                     try:
                         new_key = self.validate(key_path, skey, key)
+                        break  # Match found !
                     except Invalid, e:
                         if len(e.path) > len(key_path):
                             raise
-                        if not error or len(e.path) > len(error.path):
-                            error = e
-                        continue
-                # Backtracking is not performed once a key is selected, so if
-                # the value is invalid we immediately throw an exception.
-                try:
-                    out[new_key] = self.validate(key_path, svalue, value)
-                except Invalid, e:
-                    if len(e.path) > len(key_path):
-                        errors.append(e)
-                    else:
-                        errors.append(Invalid(e.msg + ' for dictionary value', e.path))
-                    break
-
-                # Key and value okay, mark any required() fields as found.
-                required_keys.discard(skey)
-                break
+                        error = e
+            # No matching rule ?
             else:
-                if self.extra:
+                if extra:
                     out[key] = value
                 else:
                     errors.append(Invalid('extra keys not allowed', key_path))
+                continue  # Next key
+
+            # Second, validate data against the rule we just found
+            try:
+                out[new_key] = self.validate(key_path, rule, value)
+            except Invalid, e:
+                if len(e.path) > len(key_path):
+                    errors.append(e)
+                else:
+                    errors.append(Invalid(e.msg + ' for dictionary value', e.path))
+                break
+
+            # Last, mark any required() fields as found.
+            required_keys.discard(skey)
 
         # Check that all required keys are supplied or have default values
         for key in required_keys:
-            if key.default is not None:
+            if getattr(key, 'default', None) is not None:
                 out[key] = key.default
             else:
                 errors.append(Invalid('required key not provided', path + [key]))
+
+        # handle errors and return
         if errors:
             raise InvalidList(errors)
+
         return out
 
     def _validate_list(self, path, schema, data):