diff --git a/README.rst b/README.rst index b5a9ffd..a818970 100644 --- a/README.rst +++ b/README.rst @@ -232,8 +232,8 @@ data matches: >>> from schema import Optional >>> Schema({Optional('color', default='blue'): str, - ... str: str}).validate({'texture': 'furry'}) - {'color': 'blue', 'texture': 'furry'} + ... Optional(str): str}).validate({}) + {'color': 'blue'} Defaults are used verbatim, not passed through any validators specified in the value. diff --git a/schema.py b/schema.py index 3b83cf6..43a6387 100644 --- a/schema.py +++ b/schema.py @@ -77,19 +77,29 @@ def validate(self, data): def priority(s): - """Return priority for a given object.""" + """Return the priority with which a schema should be tried against an + object. + + A priority is a tuple of at least 1 "flavor". Sorted lexically, they + determine the order in which to try schemata against data. + + """ if type(s) in (list, tuple, set, frozenset): - return ITERABLE - if type(s) is dict: - return DICT - if issubclass(type(s), type): - return TYPE - if hasattr(s, 'validate'): - return VALIDATOR - if callable(s): - return CALLABLE + ret = ITERABLE + elif type(s) is dict: + ret = DICT + elif issubclass(type(s), type): + ret = TYPE + elif hasattr(s, 'validate'): + # Deletegate to the object's priority() method if there is one, + # allowing it to traverse into itself and return a compound priority + # like (2, 3): + return getattr(s, 'priority', lambda: (VALIDATOR,))() + elif callable(s): + ret = CALLABLE else: - return COMPARABLE + ret = COMPARABLE + return (ret,) class Schema(object): @@ -104,7 +114,7 @@ def __repr__(self): def validate(self, data): s = self._schema e = self._error - flavor = priority(s) + flavor = priority(s)[0] if flavor == ITERABLE: data = Schema(type(s), error=e).validate(data) return type(s)(Or(*s, error=e).validate(d) for d in data) @@ -187,6 +197,9 @@ def validate(self, data): else: raise SchemaError('%r does not match %r' % (s, data), e) + def priority(self): + return (VALIDATOR,) + MARKER = object() @@ -200,10 +213,17 @@ def __init__(self, *args, **kwargs): 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: + if priority(self._schema)[0] != 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 + + def priority(self): + """Dig in one level so Optional('foo') takes precedence over + Optional('str'). We could go deeper if we wanted. + + """ + return super(Optional, self).priority() + priority(self._schema) diff --git a/test_schema.py b/test_schema.py index 4ec9993..5d11697 100644 --- a/test_schema.py +++ b/test_schema.py @@ -156,6 +156,13 @@ def test_dict_optional_keys(): Optional('b'): 2}).validate({'a': 1, 'b': 2}) == {'a': 1, 'b': 2} +def test_delegated_priority(): + """Make sure compound, instance-delegated priorities work.""" + # Optional('a') should take precedence, because it's more specific. + assert Schema({Optional(str): 1, + Optional('a'): 2}).validate({'a': 2}) == {'a': 2} + + def test_dict_optional_defaults(): # Optionals fill out their defaults: assert Schema({Optional('a', default=1): 11,