diff --git a/CHANGES.rst b/CHANGES.rst index 62d2ee9..63a0a77 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,8 @@ Features - Add a ``missing_msg`` argument to ``SchemaNode``, allowing customization of the error message used when the node is required and missing. +- Add `NoneOf` validator wich succeeds if the value is none of the choices. + Bug Fixes --------- diff --git a/colander/__init__.py b/colander/__init__.py index 20695ea..7e1e686 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -443,6 +443,33 @@ class OneOf(object): mapping={'val':value, 'choices':choices}) raise Invalid(node, err) + +class NoneOf(object): + """ Validator which succeeds if the value passed to it is none of a + fixed set of values. + + ``msg_err`` is used to form the ``msg`` of the :exc:`colander.Invalid` + error when reporting a validation failure. If ``msg_err`` is specified, + it must be a string. The string may contain the replacement targets + ``${choices}`` and ``${val}``, representing the set of forbidden values + and the provided value respectively. + """ + _MSG_ERR = _('"${val}" must not be one of ${choices}') + + def __init__(self, choices, msg_err=_MSG_ERR): + self.forbidden = choices + self.msg_err = msg_err + + def __call__(self, node, value): + if value not in self.forbidden: + return + + choices = ', '.join(['%s' % x for x in self.forbidden]) + err = _(self.msg_err, mapping={'val': value, 'choices': choices}) + + raise Invalid(node, err) + + class ContainsOnly(object): """ Validator which succeeds if the value passed to is a sequence and each element in the sequence is also in the sequence passed as ``choices``. diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py index 4fff3b3..aba64e9 100644 --- a/colander/tests/test_colander.py +++ b/colander/tests/test_colander.py @@ -490,6 +490,22 @@ class TestOneOf(unittest.TestCase): e = invalid_exc(validator, None, None) self.assertEqual(e.msg.interpolate(), '"None" is not one of 1, 2') + +class TestNoneOf(unittest.TestCase): + def _makeOne(self, values): + from colander import NoneOf + return NoneOf(values) + + def test_success(self): + validator = self._makeOne([1, 2]) + self.assertEqual(validator(None, 3), None) + + def test_failure(self): + validator = self._makeOne([1, 2]) + e = invalid_exc(validator, None, 2) + self.assertEqual(e.msg.interpolate(), '"2" must not be one of 1, 2') + + class TestContainsOnly(unittest.TestCase): def _makeOne(self, values): from colander import ContainsOnly diff --git a/docs/api.rst b/docs/api.rst index e3584c8..3ecc9cc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -64,6 +64,8 @@ Validators .. autoclass:: OneOf + .. autoclass:: NoneOf + .. autoclass:: ContainsOnly .. autoclass:: Function