From cfb9cdf9294db5e44a933cd5c7393bdc35175d8f Mon Sep 17 00:00:00 2001 From: Rogdham Date: Sat, 18 May 2013 17:52:09 +0100 Subject: [PATCH] Add default values handling. Fixes #12. --- README.rst | 15 +++++++++++++++ schema.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++-- test_schema.py | 33 ++++++++++++++++++++++++++++++-- 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index a03d447..19f2b94 100644 --- a/README.rst +++ b/README.rst @@ -219,6 +219,21 @@ 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 for your optional key, pass a keyword +argument ``default``, or use the ``Default`` class: + +.. code:: python + + >>> from schema import Optional, Default + >>> Schema({'event': str, + ... Optional('date'): Use(int, default=2012), + ... }).validate({'event': 'First commit'}) + {'date': 2012, 'event': 'First commit'} + >>> Schema({'account': str, + ... Optional('amount'): Default(int), + ... }).validate({'account': 'Piggy bank'}) + {'account': 'Piggy bank', 'amount': 0} + **schema** has classes ``And`` and ``Or`` that help validating several schemas for the same data: diff --git a/schema.py b/schema.py index 5a3ed3b..bda56d0 100644 --- a/schema.py +++ b/schema.py @@ -27,10 +27,40 @@ def uniq(seq): return '\n'.join(a) +def handle_default(init): + + """Add default handling to __init__ method; meant for decorators""" + + def init2(self, *args, **kw): + # find default for ``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 type(self.default) == type: + self.default = 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') @@ -59,6 +89,7 @@ def validate(self, data): class Use(object): + @handle_default def __init__(self, callable_, error=None): assert callable(callable_) self._callable = callable_ @@ -94,6 +125,7 @@ def priority(s): class Schema(object): + @handle_default def __init__(self, schema, error=None): self._schema = schema self._error = error @@ -138,12 +170,19 @@ def validate(self, data): x.autos, [e] + x.errors) else: raise SchemaError('key %r is required' % skey, e) - 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): raise SchemaError('wrong keys %r in %r' % (new, 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: @@ -178,3 +217,12 @@ def validate(self, data): class Optional(Schema): """Marker for an optional part of Schema.""" + + auto_default = True + + +class Default(Schema): + + """Wrapper automatically adding a default value if possible""" + + auto_default = True diff --git a/test_schema.py b/test_schema.py index 724f9aa..fca5a49 100644 --- a/test_schema.py +++ b/test_schema.py @@ -3,7 +3,7 @@ from pytest import raises -from schema import Schema, Use, And, Or, Optional, SchemaError +from schema import Schema, Use, And, Or, Optional, Default, SchemaError try: @@ -12,6 +12,7 @@ basestring = str # Python 3 does not have basestring +VE = raises(ValueError) SE = raises(SchemaError) @@ -56,6 +57,7 @@ def test_validate_file(): def test_and(): + with raises(AssertionError): 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 +65,11 @@ def test_and(): def test_or(): + with raises(AssertionError): 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(): @@ -138,6 +140,33 @@ def test_dict_optional_keys(): {'a': 1, 'b': 2}) == {'a': 1, 'b': 2} +def test_dict_optional_keys_defaults(): + assert Schema({'a': 1, Optional('b'): Default(2)}).validate( + {'a': 1}) == {'a': 1, 'b': 2} + assert Schema({'a': 1, Optional('b'): Default(2)}).validate( + {'a': 1}) == {'a': 1, 'b': 2} + assert Schema({'a': 1, Optional('b'): Default(int)}).validate( + {'a': 1}) == {'a': 1, 'b': 0} + assert Schema({'a': 1, Optional('b'): Default(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'): 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): Default(2)}).validate( + {'a': 1}) == {'a': 1, 0: 2} + assert Schema({'a': 1, Optional(int, default=4): Default(2)}).validate( + {'a': 1}) == {'a': 1, 4: 2} + with VE: Use(int, default='two') + with VE: Default(lambda x: x < 42, default=1337) + + def test_complex(): s = Schema({'': And([Use(open)], lambda l: len(l)), '': os.path.exists,