I want you to look the following code for validating dictionary schemas. There are some libraries out there that do the same thing, but I just wanted to write my own for fun and not to overload my current project with a big third party library.
Feel free to comment regarding code optimization, duplication, API design and the testing part.
You can see the API usage in the test snippet.
class ValidationError(Exception):
def __init__(self, *args, **kwargs):
super(ValidationError, self).__init__()
class Schema(object):
"""
Schema class to validate the structure of dic objects.
Accepts a dict of Attr's and validates their type, a validation function, etc.
usage: Schema({'name': Attr(str, validator=lambda x: len(x) > 0),
'age': Attr(int, lambda x: x > 0)})
To denote a nested dict you use it like this Attr(schema=Schema({...}))
"""
def __init__(self, attrs):
self._attrs = attrs
def bind(self, data):
if isinstance(data, dict):
self._data = data
return self
raise TypeError('data parameter is not a dict')
def validate(self, data=None):
"""
returns True if the bound data is valid to the schema
raises an exception otherwise
"""
if data:
self.bind(data)
for key in self._attrs.keys():
attr = self._attrs[key]
if not attr.is_optional and not key in self._data:
raise ValidationError('Required attribute {0} not present'.format(key))
if key in self._data:
attr.validate(self._data[key])
return True
class Attr(object):
"""
Class to be used inside a Schema object.
It validates individual items in the target dict,
usage: Attr(int, validatos=some_func)
"""
def __init__(self, t=None, optional=False, validator=None, schema=None):
self._t = t
self._optional = optional
self._validator = validator
self._schema = schema
@property
def is_schema(self):
return isinstance(self._schema, Schema)
@property
def is_optional(self):
return self._optional
@property
def validator(self):
return self._validator
@validator.setter
def validator(self, val):
self._validator = val
def validate(self, data):
if self._schema:
if not isinstance(self._schema, Schema):
raise TypeError('{0} is not instance of Schema'.format(data))
return self._schema.validate(data)
elif not isinstance(data, self._t):
raise TypeError('{0} is not instance of {1}'.format(data, self._t))
if self.validator and not self.validator(data):
raise ValidationError('attribute did not pass validation test')
return True
If you think some test are missing just tell it.
import unittest
class TestAttr(unittest.TestCase):
def test_validates_type(self):
attr = Attr(int)
self.assertTrue(attr.validate(1), 'The attribute should be valid')
attr = Attr(int)
self.assertRaises(TypeError, attr.validate, '1')
def test_validates_validator(self):
attr = Attr(int, validator=lambda x: x > 1)
self.assertTrue(attr.validate(2), 'The attribute should be valid')
self.assertRaises(ValidationError, attr.validate, 0)
class TestSchema(unittest.TestCase):
def test_flat_schema(self):
schema = Schema({'name': Attr(str),
'age': Attr(int, validator=lambda x: x > 0),
'awesome': Attr(bool, optional=True, validator=lambda x: x == True)})
valid = {'name': 'python', 'age': 19, 'awesome': True}
self.assertTrue(schema.validate(valid), 'schema should be valid')
# omit optional
valid = {'name': 'python', 'age': 19}
self.assertTrue(schema.validate(valid), 'schema should be valid')
invalid = {'name': 'python'}
self.assertRaises(ValidationError, schema.validate, invalid)
# validate optional if present
invalid = {'name': 'python', 'age': 19, 'awesome': False}
self.assertRaises(ValidationError, schema.validate, invalid)
def test_nested_schema(self):
address = Schema({'street': Attr(str, validator=lambda x: len(x) > 0),
'number': Attr(int)})
schema = Schema({'name': Attr(str),
'age': Attr(int, validator=lambda x: x > 0),
'address': Attr(schema=address)})
valid_address = {'street': 'pip', 'number': 2}
invalid_address = {'street': '', 'number': 5}
valid_data = {'name': 'python', 'age': 4, 'address': valid_address}
invalid_data = {'name': 'python', 'age': 4, 'address': invalid_address}
self.assertTrue(schema.validate(valid_data), 'schema should be valid')
self.assertRaises(ValidationError, schema.validate, invalid_data)