From 34cd54100b4b5e64e57118baec5f88014319c839 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Tue, 28 May 2013 18:57:52 -0400 Subject: [PATCH] Python3 support, thanks to Steve Leonard (@xsleonard). --- .travis.yml | 3 +- README.rst | 161 +++++++++++++++--------- tests.rst | 96 +++++++++------ voluptuous/voluptuous.py | 259 +++++++++++++++++++++++++-------------- 4 files changed, 330 insertions(+), 189 deletions(-) diff --git a/.travis.yml b/.travis.yml index 876bd68..b9b1449 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,8 @@ python: - "2.6" - "2.7" # Not quite ready for prime time... - # - "3.2" + - "3.2" + - "3.3" - "pypy" # command to install dependencies #install: "pip install -r requirements.txt --use-mirrors" diff --git a/README.rst b/README.rst index 9b65f79..ac18603 100644 --- a/README.rst +++ b/README.rst @@ -51,46 +51,65 @@ and goes a little further for completeness. "q" is required:: - >>> schema({}) - Traceback (most recent call last): - ... - MultipleInvalid: required key not provided @ data['q'] + >>> from voluptuous import MultipleInvalid + >>> try: + ... schema({}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data['q']" + True ...must be a string:: - >>> schema({'q': 123}) - Traceback (most recent call last): - ... - MultipleInvalid: expected str for dictionary value @ data['q'] + >>> try: + ... schema({'q': 123}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "expected str for dictionary value @ data['q']" + True ...and must be at least one character in length:: - >>> schema({'q': ''}) - Traceback (most recent call last): - ... - MultipleInvalid: length of value must be at least 1 for dictionary value @ data['q'] + >>> try: + ... schema({'q': ''}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "length of value must be at least 1 for dictionary value @ data['q']" + True >>> schema({'q': '#topic'}) {'q': '#topic'} "per_page" is a positive integer no greater than 20:: - >>> schema({'q': '#topic', 'per_page': 900}) - Traceback (most recent call last): - ... - MultipleInvalid: value must be at most 20 for dictionary value @ data['per_page'] - >>> schema({'q': '#topic', 'per_page': -10}) - Traceback (most recent call last): - ... - MultipleInvalid: value must be at least 1 for dictionary value @ data['per_page'] + >>> try: + ... schema({'q': '#topic', 'per_page': 900}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "value must be at most 20 for dictionary value @ data['per_page']" + True + >>> try: + ... schema({'q': '#topic', 'per_page': -10}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "value must be at least 1 for dictionary value @ data['per_page']" + True "page" is an integer >= 0:: - >>> schema({'q': '#topic', 'page': 'one'}) - Traceback (most recent call last): - ... - MultipleInvalid: expected int for dictionary value @ data['page'] - >>> schema({'q': '#topic', 'page': 1}) - {'q': '#topic', 'page': 1} + >>> try: + ... schema({'q': '#topic', 'per_page': 'one'}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "expected int for dictionary value @ data['per_page']" + True + >>> schema({'q': '#topic', 'page': 1}) == {'q': '#topic', 'page': 1} + True Defining schemas ---------------- @@ -117,10 +136,14 @@ instance of the type:: >>> schema = Schema(int) >>> schema(1) 1 - >>> schema('one') - Traceback (most recent call last): - ... - MultipleInvalid: expected int + >>> try: + ... schema('one') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "expected int" + True + Lists ~~~~~ @@ -153,10 +176,13 @@ this property. Here's an example of a date validator:: >>> schema = Schema(Date()) >>> schema('2013-03-03') datetime.datetime(2013, 3, 3, 0, 0) - >>> schema('2013-03') - Traceback (most recent call last): - ... - MultipleInvalid: not a valid value + >>> try: + ... schema('2013-03') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "not a valid value" + True In addition to simply determining if a value is valid, validators may mutate the value into a valid form. An example of this is the ``Coerce(type)`` @@ -197,10 +223,13 @@ By default any additional keys in the data, not in the schema will trigger exceptions:: >>> schema = Schema({2: 3}) - >>> schema({1: 2, 2: 3}) - Traceback (most recent call last): - ... - MultipleInvalid: extra keys not allowed @ data[1] + >>> try: + ... schema({1: 2, 2: 3}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "extra keys not allowed @ data[1]" + True This behaviour can be altered on a per-schema basis with ``Schema(..., extra=True)``:: @@ -227,18 +256,24 @@ By default, keys in the schema are not required to be in the data:: Similarly to how extra_ keys work, this behaviour can be overridden per-schema:: >>> schema = Schema({1: 2, 3: 4}, required=True) - >>> schema({3: 4}) - Traceback (most recent call last): - ... - MultipleInvalid: required key not provided @ data[1] + >>> try: + ... schema({3: 4}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data[1]" + True And per-key, with the marker token ``Required(key)``:: >>> schema = Schema({Required(1): 2, 3: 4}) - >>> schema({3: 4}) - Traceback (most recent call last): - ... - MultipleInvalid: required key not provided @ data[1] + >>> try: + ... schema({3: 4}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data[1]" + True >>> schema({1: 2}) {1: 2} @@ -249,16 +284,23 @@ using the marker token ``Optional(key)``:: >>> from voluptuous import Optional >>> schema = Schema({1: 2, Optional(3): 4}, required=True) - >>> schema({}) - Traceback (most recent call last): - ... - MultipleInvalid: required key not provided @ data[1] + >>> try: + ... schema({}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data[1]" + True >>> schema({1: 2}) {1: 2} - >>> schema({1: 2, 4: 5}) - Traceback (most recent call last): - ... - MultipleInvalid: extra keys not allowed @ data[4] + >>> try: + ... schema({1: 2, 4: 5}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "extra keys not allowed @ data[4]" + True + >>> schema({1: 2, 3: 4}) {1: 2, 3: 4} @@ -304,10 +346,13 @@ but the literal ``6`` will not match any of the elements of that list. This error will be reported back to the user immediately. No backtracking is attempted:: - >>> schema([[6]]) - Traceback (most recent call last): - ... - MultipleInvalid: invalid list value @ data[0][0] + >>> try: + ... schema([[6]]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "invalid list value @ data[0][0]" + True If we pass the data ``[6]``, the ``6`` is not a list type and so will not recurse into the first element of the schema. Matching will continue on to the diff --git a/tests.rst b/tests.rst index 23b9c14..3a480d4 100644 --- a/tests.rst +++ b/tests.rst @@ -10,25 +10,39 @@ Error reporting should be accurate:: It should show the exact index and container type, in this case a list value:: - >>> schema(['one', 'two']) - Traceback (most recent call last): - ... - MultipleInvalid: invalid list value @ data[1] + >>> try: + ... schema(['one', 'two']) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == 'invalid list value @ data[1]' + True It should also be accurate for nested values:: - >>> schema([{'two': 'nine'}]) - Traceback (most recent call last): - ... - MultipleInvalid: not a valid value for dictionary value @ data[0]['two'] - >>> schema([{'four': ['nine']}]) - Traceback (most recent call last): - ... - MultipleInvalid: invalid list value @ data[0]['four'][0] - >>> schema([{'six': {'seven': 'nine'}}]) - Traceback (most recent call last): - ... - MultipleInvalid: not a valid value for dictionary value @ data[0]['six']['seven'] + >>> try: + ... schema([{'two': 'nine'}]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "not a valid value for dictionary value @ data[0]['two']" + True + + >>> try: + ... schema([{'four': ['nine']}]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "invalid list value @ data[0]['four'][0]" + True + + >>> try: + ... schema([{'six': {'seven': 'nine'}}]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "not a valid value for dictionary value @ data[0]['six']['seven']" + True Errors should be reported depth-first:: @@ -44,19 +58,21 @@ Errors should be reported depth-first:: Voluptuous supports validation when extra fields are present in the data:: >>> schema = Schema({'one': 1, Extra: object}) - >>> schema({'two': 'two', 'one': 1}) - {'two': 'two', 'one': 1} + >>> schema({'two': 'two', 'one': 1}) == {'two': 'two', 'one': 1} + True >>> schema = Schema({'one': 1}) - >>> schema({'two': 2}) - Traceback (most recent call last): - ... - MultipleInvalid: extra keys not allowed @ data['two'] - + >>> try: + ... schema({'two': 2}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "extra keys not allowed @ data['two']" + True dict, list, and tuple should be available as type validators:: - >>> Schema(dict)({'a': 1, 'b': 2}) - {'a': 1, 'b': 2} + >>> Schema(dict)({'a': 1, 'b': 2}) == {'a': 1, 'b': 2} + True >>> Schema(list)([1,2,3]) [1, 2, 3] >>> Schema(tuple)((1,2,3)) @@ -70,8 +86,8 @@ subclasses of dict or list:: ... pass >>> >>> d = Schema(dict)(Dict(a=1, b=2)) - >>> d - {'a': 1, 'b': 2} + >>> d == {'a': 1, 'b': 2} + True >>> type(d) is Dict True >>> class List(list): @@ -131,10 +147,14 @@ Schemas built with All() should give the same error as the original validator (I ... }]) ... }) - >>> schema({'items': [{}]}) - Traceback (most recent call last): - ... - MultipleInvalid: required key not provided @ data['items'][0]['foo'] + >>> try: + ... schema({'items': [{}]}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data['items'][0]['foo']" + True + Validator should return same instance of the same type for object:: @@ -157,7 +177,7 @@ check object type:: >>> named = NamedTuple(q='one') >>> schema(named) == named True - >>> schema(named) + >>> schema(named) NamedTuple(q='one') If `cls` argument passed to object validator we should check object type:: @@ -189,10 +209,14 @@ Ensure that objects with `__slots__` supported properly:: ... >>> structure = DictStructure(q='one') >>> structure.page = 1 - >>> schema(structure) - Traceback (most recent call last): - ... - MultipleInvalid: extra keys not allowed @ data['page'] + >>> try: + ... schema(structure) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "extra keys not allowed @ data['page']" + True + >>> schema = Schema(Object({'q': 'one', Extra: object})) >>> schema(structure) DictStructure(q='one', page=1) diff --git a/voluptuous/voluptuous.py b/voluptuous/voluptuous.py index 3b4120c..a9cb869 100644 --- a/voluptuous/voluptuous.py +++ b/voluptuous/voluptuous.py @@ -75,15 +75,16 @@ ... 'Users': {'snmp_community': 'monkey'}, ... }, ... }, - ... }) # doctest: +NORMALIZE_WHITESPACE - {'set': {'snmp_version': '2c', 'snmp_community': 'public'}, - 'targets': {'exclude': ['Ping'], - 'features': {'Uptime': {'retries': 3}, - 'Users': {'snmp_community': 'monkey'}}}} + ... }) == { + ... 'set': {'snmp_version': '2c', 'snmp_community': 'public'}, + ... 'targets': { + ... 'exclude': ['Ping'], + ... 'features': {'Uptime': {'retries': 3}, + ... 'Users': {'snmp_community': 'monkey'}}}} + True """ from functools import wraps -from itertools import ifilter import os import re import sys @@ -91,8 +92,13 @@ import urllib.parse as urlparse long = int unicode = str + basestring = str + ifilter = filter + iteritems = dict.items else: + from itertools import ifilter import urlparse + iteritems = dict.iteritems __author__ = 'Alec Thomas ' @@ -221,7 +227,7 @@ def _compile_mapping(self, schema, invalid_msg=None): isinstance(key, Required)) _compiled_schema = {} - for skey, svalue in schema.iteritems(): + for skey, svalue in iteritems(schema): new_key = self._compile(skey) new_value = self._compile(svalue) _compiled_schema[skey] = (new_key, new_value) @@ -232,7 +238,7 @@ def validate_mapping(path, iterable, out): errors = [] for key, value in iterable: key_path = path + [key] - for skey, (ckey, cvalue) in _compiled_schema.iteritems(): + for skey, (ckey, cvalue) in iteritems(_compiled_schema): try: new_key = ckey(key_path, key) except Invalid as e: @@ -283,10 +289,13 @@ def _compile_object(self, schema): ... self.three = three ... >>> validate = Schema(Object({'one': 'two', 'three': 'four'}, cls=Structure)) - >>> validate(Structure(one='three')) - Traceback (most recent call last): - ... - MultipleInvalid: not a valid value for object value @ data['one'] + >>> try: + ... validate(Structure(one='three')) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "not a valid value for object value @ data['one']" + True """ base_validate = self._compile_mapping(schema, @@ -311,25 +320,35 @@ def _compile_dict(self, schema): A dictionary schema will only validate a dictionary: >>> validate = Schema({}) - >>> validate([]) - Traceback (most recent call last): - ... - MultipleInvalid: expected a dictionary + >>> try: + ... validate([]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "expected a dictionary" + True An invalid dictionary value: >>> validate = Schema({'one': 'two', 'three': 'four'}) - >>> validate({'one': 'three'}) - Traceback (most recent call last): - ... - MultipleInvalid: not a valid value for dictionary value @ data['one'] + >>> try: + ... validate({'one': 'three'}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "not a valid value for dictionary value @ data['one']" + True An invalid key: - >>> validate({'two': 'three'}) - Traceback (most recent call last): - ... - MultipleInvalid: extra keys not allowed @ data['two'] + >>> try: + ... validate({'two': 'three'}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "extra keys not allowed @ data['two']" + True + Validation function, in this case the "int" type: @@ -344,10 +363,13 @@ def _compile_dict(self, schema): purely to validate that the corresponding value is of that type. It will not Coerce the value: - >>> validate({'10': 'twenty'}) - Traceback (most recent call last): - ... - MultipleInvalid: extra keys not allowed @ data['10'] + >>> try: + ... validate({'10': 'twenty'}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "extra keys not allowed @ data['10']" + True Wrap them in the Coerce() function to achieve this: @@ -359,10 +381,13 @@ def _compile_dict(self, schema): Custom message for required key >>> validate = Schema({Required('one', 'required'): 'two'}) - >>> validate({}) - Traceback (most recent call last): - ... - MultipleInvalid: required @ data['one'] + >>> try: + ... validate({}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required @ data['one']" + True (This is to avoid unexpected surprises.) """ @@ -374,7 +399,7 @@ def validate_dict(path, data): raise Invalid('expected a dictionary', path) out = type(data)() - return base_validate(path, data.iteritems(), out) + return base_validate(path, iteritems(data), out) return validate_dict @@ -386,10 +411,13 @@ def _compile_sequence(self, schema, seq_type): >>> validator = Schema(['one', 'two', int]) >>> validator(['one']) ['one'] - >>> validator([3.5]) - Traceback (most recent call last): - ... - MultipleInvalid: invalid list value @ data[0] + >>> try: + ... validator([3.5]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == 'invalid list value @ data[0]' + True >>> validator([1]) [1] """ @@ -436,10 +464,13 @@ def _compile_tuple(self, schema): >>> validator = Schema(('one', 'two', int)) >>> validator(('one',)) ('one',) - >>> validator((3.5,)) - Traceback (most recent call last): - ... - MultipleInvalid: invalid tuple value @ data[0] + >>> try: + ... validator((3.5,)) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == 'invalid tuple value @ data[0]' + True >>> validator((1,)) (1,) """ @@ -453,10 +484,13 @@ def _compile_list(self, schema): >>> validator = Schema(['one', 'two', int]) >>> validator(['one']) ['one'] - >>> validator([3.5]) - Traceback (most recent call last): - ... - MultipleInvalid: invalid list value @ data[0] + >>> try: + ... validator([3.5]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == 'invalid list value @ data[0]' + True >>> validator([1]) [1] """ @@ -470,10 +504,13 @@ def _compile_scalar(schema): >>> _compile_scalar(int)([], 1) 1 - >>> _compile_scalar(float)([], '1') - Traceback (most recent call last): - ... - Invalid: expected float + >>> try: + ... _compile_scalar(float)([], '1') + ... raise AssertionError('Invalid not raised') + ... except Invalid as e: + ... exc = e + >>> str(exc) == 'expected float' + True Callables have >>> _compile_scalar(lambda v: float(v))([], '1') @@ -481,10 +518,13 @@ def _compile_scalar(schema): As a convenience, ValueError's are trapped: - >>> _compile_scalar(lambda v: float(v))([], 'a') - Traceback (most recent call last): - ... - Invalid: not a valid value + >>> try: + ... _compile_scalar(lambda v: float(v))([], 'a') + ... raise AssertionError('Invalid not raised') + ... except Invalid as e: + ... exc = e + >>> str(exc) == 'not a valid value' + True """ if isinstance(schema, type): def validate_instance(path, data): @@ -524,7 +564,7 @@ def _iterate_object(obj): # maybe we have named tuple here? if hasattr(obj, '_asdict'): d = obj._asdict() - for item in d.iteritems(): + for item in iteritems(d): yield item try: slots = obj.__slots__ @@ -592,18 +632,24 @@ def Msg(schema, msg): >>> validate = Schema( ... Msg(['one', 'two', int], ... 'should be one of "one", "two" or an integer')) - >>> validate(['three']) - Traceback (most recent call last): - ... - MultipleInvalid: should be one of "one", "two" or an integer + >>> try: + ... validate(['three']) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == 'should be one of "one", "two" or an integer' + True Messages are only applied to invalid direct descendants of the schema: >>> validate = Schema(Msg([['one', 'two', int]], 'not okay!')) - >>> validate([['three']]) - Traceback (most recent call last): - ... - MultipleInvalid: invalid list value @ data[0][0] + >>> try: + ... validate([['three']]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == 'invalid list value @ data[0][0]' + True """ schema = Schema(schema) @@ -629,18 +675,24 @@ def message(default=None): ... return int(v) >>> validate = Schema(isint()) - >>> validate('a') - Traceback (most recent call last): - ... - MultipleInvalid: not an integer + >>> try: + ... validate('a') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == 'not an integer' + True The message can be overridden on a per validator basis: >>> validate = Schema(isint('bad')) - >>> validate('a') - Traceback (most recent call last): - ... - MultipleInvalid: bad + >>> try: + ... validate('a') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == 'bad' + True """ def decorator(f): @wraps(f) @@ -665,10 +717,13 @@ def truth(f): >>> validate = Schema(isdir) >>> validate('/') '/' - >>> validate('/notavaliddir') - Traceback (most recent call last): - ... - MultipleInvalid: not a valid value + >>> try: + ... validate('/notavaliddir') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == 'not a valid value' + True """ @wraps(f) def check(v): @@ -704,16 +759,22 @@ def IsTrue(v): "In the Python sense" means that implicitly false values, such as empty lists, dictionaries, etc. are treated as "false": - >>> validate([]) - Traceback (most recent call last): - ... - MultipleInvalid: value was not true + >>> try: + ... validate([]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "value was not true" + True >>> validate([1]) [1] - >>> validate(False) - Traceback (most recent call last): - ... - MultipleInvalid: value was not true + >>> try: + ... validate(False) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "value was not true" + True ...and so on. """ @@ -745,10 +806,13 @@ def Boolean(v): >>> validate = Schema(Boolean()) >>> validate(True) True - >>> validate('moo') - Traceback (most recent call last): - ... - MultipleInvalid: expected boolean + >>> try: + ... validate('moo') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "expected boolean" + True """ if isinstance(v, basestring): v = v.lower() @@ -772,10 +836,14 @@ def Any(*validators, **kwargs): 'true' >>> validate(1) True - >>> validate('moo') - Traceback (most recent call last): - ... - MultipleInvalid: no valid value found + >>> try: + ... validate('moo') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "no valid value found" + True + """ msg = kwargs.pop('msg', None) schemas = [Schema(val) for val in validators] @@ -824,10 +892,13 @@ def Match(pattern, msg=None): >>> validate = Schema(Match(r'^0x[A-F0-9]+$')) >>> validate('0x123EF4') '0x123EF4' - >>> validate('123EF4') - Traceback (most recent call last): - ... - MultipleInvalid: does not match regular expression + >>> try: + ... validate('123EF4') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "does not match regular expression" + True >>> validate(123) Traceback (most recent call last):