Skip to content

Commit

Permalink
Add default values handling. Fixes keleshev#12.
Browse files Browse the repository at this point in the history
  • Loading branch information
Rogdham committed May 18, 2013
1 parent 43dbb1c commit cfb9cdf
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 4 deletions.
15 changes: 15 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
52 changes: 50 additions & 2 deletions schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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_
Expand Down Expand Up @@ -94,6 +125,7 @@ def priority(s):

class Schema(object):

@handle_default
def __init__(self, schema, error=None):
self._schema = schema
self._error = error
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
33 changes: 31 additions & 2 deletions test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -12,6 +12,7 @@
basestring = str # Python 3 does not have basestring


VE = raises(ValueError)
SE = raises(SchemaError)


Expand Down Expand Up @@ -56,18 +57,19 @@ 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
with SE: And(Use(int), lambda n: 0 < n < 5).validate('3.33')


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():
Expand Down Expand Up @@ -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({'<file>': And([Use(open)], lambda l: len(l)),
'<path>': os.path.exists,
Expand Down

0 comments on commit cfb9cdf

Please sign in to comment.