From e36550d3f027bd5c15af194b397c0a546bd84f7b Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Fri, 27 Feb 2015 12:05:11 -0500 Subject: [PATCH 1/2] Prioritize Optionals based on one level of depth traversal. Close #44, #45. This enables the common pattern in which there's a 'foo' key with a default and we also take arbitrary other, optional str keys. We turn priority() into a delegating "generic function", like len(). This lays the groundwork to go deeper--with any "flavor", not just Optionals. First, we'd want to refactor Schema.validate() so it constructs actual Schema subclasses all the time rather than handling many cases inline. For sets, for example, it would construct an Iterable, and Iterable.priority() would know how to tunnel one level inside itself to return a compound priority. Then we could prioritize Optional((1, 0)) over Optional((int,)). The behavior of And() and Or() (and iterables) are up for grabs: perhaps go with the priority of the most specific item inside? --- schema.py | 46 +++++++++++++++++++++++++++++++++------------- test_schema.py | 7 +++++++ 2 files changed, 40 insertions(+), 13 deletions(-) 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, From 99dc4130f0f05fd3c2d4bc6663a2419851f3c90f Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Fri, 27 Feb 2015 12:23:24 -0500 Subject: [PATCH 2/2] Fix a nondeterministically failing doctest. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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.