Skip to content

Instantly share code, notes, and snippets.

@andreypopp
Created May 28, 2011 11:09
Show Gist options
  • Save andreypopp/996795 to your computer and use it in GitHub Desktop.
Save andreypopp/996795 to your computer and use it in GitHub Desktop.
""" Validating Python data structures."""
__all__ = (
"ValidationError",
"Type", "Or", "And", "Optional", "NullType", "Nullable", "Null",
"IntType", "StringType", "BoolType",
"Int", "String", "Bool",
"Sequence", "Mapping")
class ValidationError(TypeError):
pass
class Type(object):
""" Validates any value.
>>> Any.validate(1)
>>> Any.validate("1")
>>> Any.validate(True)
"""
def validate(self, value):
pass
def __or__(self, type):
return Or(self, type)
def __and__(self, type):
return And(self, type)
Any = Type()
class Or(Type):
""" Represent sum type.
>>> schema = Or(Int, String)
>>> schema.validate(1)
>>> schema.validate("1")
>>> schema.validate([])
Traceback (most recent call last):
...
ValidationError
There's also different syntax for constructing sum types::
>>> schema = Int | String
>>> schema.validate(1)
>>> schema.validate("1")
>>> schema.validate([])
Traceback (most recent call last):
...
ValidationError
"""
def __init__(self, left, right):
if isinstance(left, type) and issubclass(left, Type):
left = left()
if isinstance(right, type) and issubclass(right, Type):
right = right()
self.left = left
self.right = right
def validate(self, value):
try:
self.left.validate(value)
except ValidationError:
self.right.validate(value)
class And(Type):
""" Represent logical AND of types.
>>> schema = And(Int, Bool)
>>> schema.validate(True)
>>> schema.validate(1)
Traceback (most recent call last):
...
ValidationError
There's also different syntax for constructing sum types::
>>> schema = Int & Bool
>>> schema.validate(True)
>>> schema.validate(1)
Traceback (most recent call last):
...
ValidationError
"""
def __init__(self, left, right):
if isinstance(left, type) and issubclass(left, Type):
left = left()
if isinstance(right, type) and issubclass(right, Type):
right = right()
self.left = left
self.right = right
def validate(self, value):
self.left.validate(value)
self.right.validate(value)
class Optional(Type):
""" Type, which is optional.
This kind of value behaves exactly like ``Type``, but treated differently
by containers::
>>> schema = Optional(Int)
>>> schema.validate(1)
>>> schema.validate("1")
Traceback (most recent call last):
...
ValidationError
"""
def __init__(self, node):
if isinstance(node, type) and issubclass(node, Type):
node = node()
self.node = node
def validate(self, value):
self.node.validate(value)
class NullType(Type):
""" Validates only ``None``::
>>> Null.validate(None)
>>> Null.validate(1)
Traceback (most recent call last):
...
ValidationError
"""
def validate(self, value):
if not value is None:
raise ValidationError()
Null = NullType()
Nullable = lambda type: type | Null
class IntType(Type):
""" Integer type.
>>> Int.validate(1)
>>> Int.validate("1")
Traceback (most recent call last):
...
ValidationError
>>> (Int > 5).validate(6)
>>> (Int > 5).validate(3)
Traceback (most recent call last):
...
ValidationError: 3 < 5
"""
def __init__(self, min=None, max=None):
self.min = min
self.max = max
def validate(self, value):
if not isinstance(value, (int, long)):
raise ValidationError()
if not self.max is None and value > self.max:
raise ValidationError("%s > %s" % (value, self.max))
if not self.min is None and value < self.min:
raise ValidationError("%s < %s" % (value, self.min))
def __gt__(self, value):
return Int(min=value, max=self.max)
def __lt__(self, value):
return Int(min=self.min, max=value)
def __call__(self, min=None, max=None):
return IntType(min=min, max=max)
Int = IntType()
class BoolType(Type):
""" Validates only ``bool``::
>>> Bool.validate(False)
>>> Bool.validate(True)
>>> Bool.validate(1)
Traceback (most recent call last):
...
ValidationError
"""
def validate(self, value):
if not isinstance(value, bool):
raise ValidationError()
def __call__(self):
return BoolType()
Bool = BoolType()
class StringType(Type):
""" Validates ``basestring`` type::
>>> String.validate("string")
>>> String.validate(u"string")
>>> String.validate(1)
Traceback (most recent call last):
...
ValidationError
"""
def validate(self, value):
if not isinstance(value, basestring):
raise ValidationError()
String = StringType()
class Sequence(Type):
""" Sequence represent iterable sequence.
>>> schema = Sequence(Int)
>>> schema.validate([1, 2, 3])
>>> schema.validate([1, "2", 3])
Traceback (most recent call last):
...
ValidationError
"""
def __init__(self, node):
if isinstance(node, type) and issubclass(node, Type):
node = node()
self.node = node
def validate(self, value):
if not hasattr(value, "__iter__"):
raise ValidationError("%r is not iterable" % value)
for item in value:
self.node.validate(item)
class Mapping(Type):
""" Mapping represents disrete mapping, such as dictionary.
>>> schema = Mapping(a=Optional(Int), b=String)
>>> schema.validate({"a": 1, "b": "string"})
>>> schema.validate({"b": "string"})
>>> schema.validate({"a": 1})
Traceback (most recent call last):
...
ValidationError: b is missing
>>> schema.validate({"b": 1})
Traceback (most recent call last):
...
ValidationError
"""
def __init__(self, **nodes):
for name, node in nodes.items():
if isinstance(node, type) and issubclass(node, Type):
node = node()
nodes[name] = node
self.nodes = nodes
def validate(self, value):
if not hasattr(value, "__getitem__"):
raise ValidationError("%r is not indexable" % value)
for name, node in self.nodes.items():
try:
node_value = value[name]
except KeyError:
if not isinstance(node, Optional):
raise ValidationError("%s is missing" % name)
else:
node.validate(node_value)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment