diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 0785d48..8af39a1 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -6,7 +6,7 @@ Schema, Required, Optional, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, - validate, ExactSequence, Equal, Unordered, Number, Date, Datetime + validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date ) from voluptuous.humanize import humanize_error from voluptuous.util import to_utf8_py2, u @@ -371,6 +371,7 @@ def test_repr(): max_included=False, msg='number not in range') coerce_ = Coerce(int, msg="moo") all_ = All('10', Coerce(int), msg='all msg') + maybe_int = Maybe(int) assert_equal(repr(match), "Match('a pattern', msg='message')") assert_equal(repr(replace), "Replace('you', 'I', msg='you and I')") @@ -380,6 +381,7 @@ def test_repr(): ) assert_equal(repr(coerce_), "Coerce(int, msg='moo')") assert_equal(repr(all_), "All('10', Coerce(int, msg=None), msg='all msg')") + assert_equal(repr(maybe_int), "Maybe(%s)" % str(int)) def test_list_validation_messages(): @@ -499,6 +501,16 @@ def test_unordered(): s([3, 2]) +def test_maybe(): + assert_raises(TypeError, Maybe, lambda x: x) + + s = Schema(Maybe(int)) + assert s(1) == 1 + assert s(None) is None + + assert_raises(Invalid, s, 'foo') + + def test_empty_list_as_exact(): s = Schema([]) assert_raises(Invalid, s, [1]) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index f7ef2da..08fb0bf 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -459,6 +459,35 @@ def PathExists(v): raise PathInvalid("Not a Path") +class Maybe(object): + """Validate that the object is of a given type or is None. + + :raises Invalid: if the value is not of the type declared and is not None + + >>> s = Schema(Maybe(int)) + >>> s(10) + 10 + >>> with raises(Invalid): + ... s("string") + + """ + def __init__(self, kind, msg=None): + if not isinstance(kind, type): + raise TypeError("kind has to be a type") + + self.kind = kind + self.msg = msg + + def __call__(self, v): + if v is not None and not isinstance(v, self.kind): + raise Invalid(self.msg or "%s must be None or of type %s" % (v, self.kind)) + + return v + + def __repr__(self): + return 'Maybe(%s)' % str(self.kind) + + class Range(object): """Limit a value to a range.