Skip to content

Commit

Permalink
Merge pull request #52 from erikrose/optional-defaults
Browse files Browse the repository at this point in the history
Support defaults via Optional
  • Loading branch information
keleshev committed Feb 27, 2015
2 parents ce104db + 7025552 commit cffb3f3
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 19 deletions.
13 changes: 13 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,19 @@ You can mark a key as optional as follows:
... Optional('occupation'): str}).validate({'name': 'Sam'})
{'name': 'Sam'}
``Optional`` keys can also carry a ``default``, to be used when no key in the
data matches:

.. code:: python
>>> from schema import Optional
>>> Schema({Optional('color', default='blue'): str,
... str: str}).validate({'texture': 'furry'})
{'color': 'blue', 'texture': 'furry'}
Defaults are used verbatim, not passed through any validators specified in the
value.

**schema** has classes ``And`` and ``Or`` that help validating several schemas
for the same data:

Expand Down
66 changes: 47 additions & 19 deletions schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,23 @@ def validate(self, data):
raise SchemaError('%s(%r) raised %r' % (f, data, x), self._error)


COMPARABLE, CALLABLE, VALIDATOR, TYPE, DICT, ITERABLE = range(6)


def priority(s):
"""Return priority for a give object."""
"""Return priority for a given object."""
if type(s) in (list, tuple, set, frozenset):
return 6
return ITERABLE
if type(s) is dict:
return 5
if hasattr(s, 'validate'):
return 4
return DICT
if issubclass(type(s), type):
return 3
return TYPE
if hasattr(s, 'validate'):
return VALIDATOR
if callable(s):
return 2
return CALLABLE
else:
return 1
return COMPARABLE


class Schema(object):
Expand All @@ -101,14 +104,16 @@ def __repr__(self):
def validate(self, data):
s = self._schema
e = self._error
if type(s) in (list, tuple, set, frozenset):
flavor = priority(s)
if flavor == ITERABLE:
data = Schema(type(s), error=e).validate(data)
return type(s)(Or(*s, error=e).validate(d) for d in data)
if type(s) is dict:
if flavor == DICT:
data = Schema(dict, error=e).validate(data)
new = type(data)() # new - is a dict of the validated values
x = None
coverage = set() # non-optional schema keys that were matched
covered_optionals = set()
# for each key and value find a schema entry matching them, if any
sorted_skeys = list(sorted(s, key=priority))
for key, value in data.items():
Expand All @@ -127,7 +132,8 @@ def validate(self, data):
x = _x
raise
else:
coverage.add(skey)
(covered_optionals if type(skey) is Optional
else coverage).add(skey)
valid = True
break
if valid:
Expand All @@ -136,7 +142,6 @@ 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:
raise SchemaError('missed keys %r' % (required - coverage), e)
Expand All @@ -145,21 +150,28 @@ def validate(self, data):
s_wrong_keys = ', '.join('%r' % k for k in sorted(wrong_keys))
raise SchemaError('wrong keys %s in %r' % (s_wrong_keys, data),
e)

# Apply default-having optionals that haven't been used:
defaults = set(k for k in s if type(k) is Optional and
hasattr(k, 'default')) - covered_optionals
for default in defaults:
new[default.key] = default.default

return new
if hasattr(s, 'validate'):
if flavor == TYPE:
if isinstance(data, s):
return data
else:
raise SchemaError('%r should be instance of %r' % (data, s), e)
if flavor == VALIDATOR:
try:
return s.validate(data)
except SchemaError as x:
raise SchemaError([None] + x.autos, [e] + x.errors)
except BaseException as x:
raise SchemaError('%r.validate(%r) raised %r' % (s, data, x),
self._error)
if issubclass(type(s), type):
if isinstance(data, s):
return data
else:
raise SchemaError('%r should be instance of %r' % (data, s), e)
if callable(s):
if flavor == CALLABLE:
f = s.__name__
try:
if s(data):
Expand All @@ -176,6 +188,22 @@ def validate(self, data):
raise SchemaError('%r does not match %r' % (s, data), e)


MARKER = object()


class Optional(Schema):

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

def __init__(self, *args, **kwargs):
default = kwargs.pop('default', MARKER)
super(Optional, self).__init__(*args, **kwargs)
if default is not MARKER:
# See if I can come up with a static key to use for myself:
if priority(self._schema) != COMPARABLE:
raise TypeError(
'Optional keys with defaults must have simple, '
'predictable values, like literal strings or ints. '
'"%r" is too complex.' % (self._schema,))
self.default = default
self.key = self._schema
17 changes: 17 additions & 0 deletions test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,23 @@ 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}
# Make sure Optionals are favored over types:
assert Schema({basestring: 1,
Optional('b'): 2}).validate({'a': 1, 'b': 2}) == {'a': 1, 'b': 2}


def test_dict_optional_defaults():
# Optionals fill out their defaults:
assert Schema({Optional('a', default=1): 11,
Optional('b', default=2): 22}).validate({'a': 11}) == {'a': 11, 'b': 2}

# Optionals take precedence over types. Here, the "a" is served by the
# Optional:
assert Schema({Optional('a', default=1): 11,
basestring: 22}).validate({'b': 22}) == {'a': 1, 'b': 22}

with raises(TypeError):
Optional(And(str, Use(int)), default=7)


def test_complex():
Expand Down

0 comments on commit cffb3f3

Please sign in to comment.