diff --git a/CHANGES.txt b/CHANGES.txt index a177dae..95a4c01 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,11 @@ +Next release +------------ + +Features +~~~~~~~~ + +- Add ``colander.ContainsOnly`` and ``colander.url`` validators. + 1.0a1 (2013-01-10) ------------------ diff --git a/colander/__init__.py b/colander/__init__.py index 5397336..f7bb024 100644 --- a/colander/__init__.py +++ b/colander/__init__.py @@ -1,3 +1,5 @@ +# coding=utf-8 + import datetime import decimal import time @@ -356,6 +358,24 @@ class OneOf(object): 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 ``acceptable`` + """ + err_template = _( + 'One or more of the choices you made was not acceptable' + ) + def __init__(self, acceptable): + self.acceptable = set(acceptable) + + def __call__(self, node, value): + if not set(value).issubset(self.acceptable): + err = _( + self.err_template, + mapping = {'val':value, 'acceptable':self.acceptable} + ) + raise Invalid(node, err) + def luhnok(node, value): """ Validator which checks to make sure that the value passes a luhn mod-10 checksum (credit cards). ``value`` must be a string, not an @@ -386,6 +406,10 @@ def _luhnok(value): sum = sum + digit return sum +URL_REGEX = r"""(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""" # "emacs! + +url = Regex(URL_REGEX, _('Must be a URL')) + class SchemaType(object): """ Base class for all schema types """ def flatten(self, node, appstruct, prefix='', listitem=False): diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py index 695978f..6f962dc 100644 --- a/colander/tests/test_colander.py +++ b/colander/tests/test_colander.py @@ -408,6 +408,30 @@ class TestOneOf(unittest.TestCase): e = invalid_exc(validator, None, None) self.assertEqual(e.msg.interpolate(), '"None" is not one of 1, 2') +class TestContainsOnly(unittest.TestCase): + def _makeOne(self, values): + from colander import ContainsOnly + return ContainsOnly(values) + + def test_success(self): + validator = self._makeOne([1]) + self.assertEqual(validator(None, [1]), None) + + def test_failure(self): + validator = self._makeOne([1]) + e = invalid_exc(validator, None, [2]) + self.assertEqual( + e.msg.interpolate(), + 'One or more of the choices you made was not acceptable' + ) + + def test_failure_with_custom_error_template(self): + validator = self._makeOne([1]) + from colander import _ + validator.err_template = _('${val}: ${acceptable}') + e = invalid_exc(validator, None, [2]) + self.assertTrue('[2]' in e.msg.interpolate()) + class Test_luhnok(unittest.TestCase): def _callFUT(self, node, value): from colander import luhnok @@ -432,6 +456,21 @@ class Test_luhnok(unittest.TestCase): val = '4111111111111111' self.assertFalse(self._callFUT(None, val)) +class Test_url_validator(unittest.TestCase): + def _callFUT(self, val): + from colander import url + return url(None, val) + + def test_it_success(self): + val = 'http://example.com' + result = self._callFUT(val) + self.assertEqual(result, None) + + def test_it_failure(self): + val = 'not-a-url' + from colander import Invalid + self.assertRaises(Invalid, self._callFUT, val) + class TestSchemaType(unittest.TestCase): def _makeOne(self, *arg, **kw): from colander import SchemaType diff --git a/docs/api.rst b/docs/api.rst index c8471ae..2562d94 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -59,6 +59,8 @@ Validators .. autoclass:: OneOf + .. autoclass:: ContainsOnly + .. autoclass:: Function .. autoclass:: Regex @@ -67,6 +69,10 @@ Validators .. autofunction:: luhnok + .. attribute:: url + + A validator which ensures the value is a URL (via regex). + Types ~~~~~