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 a647f74
Show file tree
Hide file tree
Showing 3 changed files with 92 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'})
{'date': 2012, 'comment': 'Initial import', '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
46 changes: 44 additions & 2 deletions schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,41 @@ 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]
while hasattr(self.default, 'default'):
self.default = self.default.default
if issubclass(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 @@ -55,6 +86,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 +123,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 +169,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 +219,5 @@ def validate(self, data):
class Optional(Schema):

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

auto_default = True
27 changes: 26 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 @@ -153,6 +156,28 @@ 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'): 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)


def test_complex():
s = Schema({'<file>': And([Use(open)], lambda l: len(l)),
'<path>': os.path.exists,
Expand Down

0 comments on commit a647f74

Please sign in to comment.