diff --git a/README.rst b/README.rst index fcb0891..06db3f6 100644 --- a/README.rst +++ b/README.rst @@ -225,6 +225,28 @@ You can mark a key as optional as follows: ... Optional('occupation'): str}).validate({'name': 'Sam'}) {'name': 'Sam'} +And if you want to give a default value, simply use the keyword argument +``default``: + +.. code:: python + + >>> from schema import Optional, Use, Or + >>> Schema({'event': str, + ... # use the default argument for schema classes + ... Optional('date'): Use(int, default=2012), + ... # wrap other objects in a Schema class to add a default value + ... Optional('comment'): Schema(str, default='Initial import'), + ... }).validate({'event': 'First commit'}) + {'comment': 'Initial import', 'date': 2012, 'event': 'First commit'} + >>> # advanced use: + ... Schema({ + ... # Optional key is a type, it gets instantiated as a default + ... Optional(int): Or(True, False, default=False), + ... # but it is possible to use the default attribute as well + ... Optional(int, default=42): Use(str, default='The answer'), + ... }).validate({}) + {0: False, 42: 'The answer'} + **schema** has classes ``And`` and ``Or`` that help validating several schemas for the same data: diff --git a/schema.py b/schema.py index 534ae9e..f09b246 100644 --- a/schema.py +++ b/schema.py @@ -23,10 +23,44 @@ def uniq(seq): return '\n'.join(a) +def handle_default(init): + + """Add default handling to the __init__ method + Meant to be used as a decorator""" + + def init2(self, *args, **kw): + # get default from the ``default`` keyword argument + if 'default' in kw: + self.default = kw['default'] + del(kw['default']) + # if auto_default is set, get default from first argument + elif hasattr(self, 'auto_default') and self.auto_default: + self.default = args[0] + if hasattr(self.default, 'default'): + self.default = self.default.default + elif issubclass(type(self.default), type): + self.default = self.default() + elif hasattr(self.default, 'validate'): + # e.g. {Optional(Use(int)): ...} + delattr(self, 'default') + # normal init + init(self, *args, **kw) + # validate default + if hasattr(self, 'default'): + try: + self.default = self.validate(self.default) + except SchemaError: + raise ValueError('%s does not validate its default: %s' % ( + self, self.default)) + return init2 + + class And(object): + @handle_default def __init__(self, *args, **kw): self._args = args + assert len(args) assert list(kw) in (['error'], []) self._error = kw.get('error') @@ -55,6 +89,7 @@ def validate(self, data): class Use(object): + @handle_default def __init__(self, callable_, error=None): assert callable(callable_) self._callable = callable_ @@ -91,6 +126,7 @@ def priority(s): class Schema(object): + @handle_default def __init__(self, schema, error=None): self._schema = schema self._error = error @@ -136,15 +172,22 @@ def validate(self, data): if x is not None: raise SchemaError(['invalid value for key %r' % key] + x.autos, [e] + x.errors) - coverage = set(k for k in coverage if type(k) is not Optional) required = set(k for k in s if type(k) is not Optional) - if coverage != required: + # missed keys + if not required.issubset(coverage): raise SchemaError('missed keys %r' % (required - coverage), e) + # wrong keys if len(new) != len(data): wrong_keys = set(data.keys()) - set(new.keys()) s_wrong_keys = ', '.join('%r' % k for k in sorted(wrong_keys)) raise SchemaError('wrong keys %s in %r' % (s_wrong_keys, data), e) + # default for optional keys + for k in set(s) - required - coverage: + try: + new[k.default] = s[k].default + except AttributeError: + pass return new if hasattr(s, 'validate'): try: @@ -179,3 +222,5 @@ def validate(self, data): class Optional(Schema): """Marker for an optional part of Schema.""" + + auto_default = True diff --git a/test_schema.py b/test_schema.py index 079185e..df15366 100644 --- a/test_schema.py +++ b/test_schema.py @@ -12,6 +12,8 @@ basestring = str # Python 3 does not have basestring +AE = raises(AssertionError) +VE = raises(ValueError) SE = raises(SchemaError) @@ -56,6 +58,7 @@ def test_validate_file(): def test_and(): + with AE: And() assert And(int, lambda n: 0 < n < 5).validate(3) == 3 with SE: And(int, lambda n: 0 < n < 5).validate(3.33) assert And(Use(int), lambda n: 0 < n < 5).validate(3.33) == 3 @@ -63,11 +66,11 @@ def test_and(): def test_or(): + with AE: Or() assert Or(int, dict).validate(5) == 5 assert Or(int, dict).validate({}) == {} with SE: Or(int, dict).validate('hai') assert Or(int).validate(4) - with SE: Or().validate(2) def test_validate_list(): @@ -151,6 +154,35 @@ def test_dict_optional_keys(): assert Schema({'a': 1, Optional('b'): 2}).validate({'a': 1}) == {'a': 1} assert Schema({'a': 1, Optional('b'): 2}).validate( {'a': 1, 'b': 2}) == {'a': 1, 'b': 2} + assert Schema({'a': 1, Optional(Use(int)): 2}).validate( + {'a': 1, '4': 2}) == {'a': 1, 4: 2} + + +def test_dict_optional_keys_defaults(): + assert Schema({'a': 1, Optional('b'): Schema(2, default=2)}).validate( + {'a': 1}) == {'a': 1, 'b': 2} + assert Schema({'a': 1, Optional('b'): Use(int, default='4')}).validate( + {'a': 1}) == {'a': 1, 'b': 4} + assert Schema({'a': 1, Optional('b'): Use(int, default=4)}).validate( + {'a': 1}) == {'a': 1, 'b': 4} + assert Schema({'a': 1, Optional('b'): Or(2, 4, default=4)}).validate( + {'a': 1}) == {'a': 1, 'b': 4} + assert Schema({'a': 1, Optional('b'): And( + lambda x: x > 0, + lambda x: x < 16, + default=4)}).validate({'a': 1}) == {'a': 1, 'b': 4} + assert Schema({'a': 1, Optional(int): Schema(2, default=2)}).validate( + {'a': 1}) == {'a': 1, 0: 2} + assert Schema({ + 'a': 1, + Optional(int, default=4): Schema(2, default=2) + }).validate({'a': 1}) == {'a': 1, 4: 2} + with VE: Use(int, default='two') + with VE: Schema(lambda x: x < 42, default=1337) + assert Schema({ + 'a': 1, + Optional(Use(int, default=4)): Schema(int, default=2) + }).validate({'a': 1}) == {'a': 1, 4: 2} def test_complex():