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 Apr 30, 2014
1 parent b81c544 commit a7314c3
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 3 deletions.
22 changes: 22 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

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

This comment has been minimized.

Copy link
@chadrik

chadrik May 18, 2014

why not kw.pop('default') instead of these two lines?

# 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()

This comment has been minimized.

Copy link
@chadrik

chadrik May 18, 2014

This is quite likely to be the source of a lot of errors, so I think it would be a good idea to provide a try/except here with some explanation. Something like: "Could not automatically determine default from type %s. Provide explicit default with default keyword."

I really like that the Schema class does almost nothing on instantiation. If possible it would be nice to delay this auto-default behavior until validate is called.

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)

This comment has been minimized.

Copy link
@chadrik

chadrik May 18, 2014

Since schema's can be quite complex this could be a pretty heavy operation to do on __init__. What do you think about delaying that until the whole schema is validated?

except SchemaError:
raise ValueError('%s does not validate its default: %s' % (

This comment has been minimized.

Copy link
@chadrik

chadrik May 18, 2014

I think this should be a SchemaError. You could take an approach similar to the Or class and prefix the validation error with something specific to this context.

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 @@ -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_
Expand Down Expand Up @@ -91,6 +126,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 @@ -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:
Expand Down Expand Up @@ -179,3 +222,5 @@ def validate(self, data):
class Optional(Schema):

"""Marker for an optional part of Schema."""

auto_default = True
34 changes: 33 additions & 1 deletion test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
basestring = str # Python 3 does not have basestring


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


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


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():
Expand Down Expand Up @@ -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():
Expand Down

0 comments on commit a7314c3

Please sign in to comment.