From 37b67a303fb446851984a732ec7fb9cf657c0077 Mon Sep 17 00:00:00 2001 From: Jake VanderPlas Date: Mon, 5 Mar 2018 13:01:24 -0800 Subject: [PATCH 1/2] ENH: wrap to_dict() tracebacks for more user-friendly errors --- altair/utils/schemapi.py | 47 ++++++++++++++++++++++++++++++++++---- altair/vegalite/api.py | 2 +- altair/vegalite/schema.py | 2 +- altair/vegalite/v2/api.py | 17 ++++++++++++-- tools/schemapi/schemapi.py | 47 ++++++++++++++++++++++++++++++++++---- 5 files changed, 101 insertions(+), 14 deletions(-) diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py index 404f8c46b..5a0696997 100644 --- a/altair/utils/schemapi.py +++ b/altair/utils/schemapi.py @@ -3,8 +3,10 @@ import collections import contextlib import json +import textwrap import jsonschema +import six # If DEBUG_MODE is True, then schema objects are converted to dict and @@ -36,6 +38,35 @@ def debug_mode(arg): DEBUG_MODE = original +class SchemaValidationError(jsonschema.ValidationError): + """A wrapper for jsonschema.ValidationError with friendlier traceback""" + def __init__(self, obj, err): + super(SchemaValidationError, self).__init__(**err._contents()) + self.obj = obj + + def __unicode__(self): + cls = self.obj.__class__ + schema_path = ['{0}.{1}'.format(cls.__module__,cls.__name__)] + schema_path.extend(self.schema_path) + schema_path = ' ->'.join(val for val in schema_path[:-1] + if val not in ('properties', + 'additionalProperties', + 'patternProperties')) + return """Invalid specification + + validator {1!r} in {0} + + {2} + """.format(schema_path, self.validator, self.message) + + if six.PY3: + __str__ = __unicode__ + else: + def __str__(self): + return unicode(self).encode("utf-8") + + + class UndefinedType(object): """A singleton object for marking undefined attributes""" __instance = None @@ -146,9 +177,11 @@ def to_dict(self, validate=True, ignore=[], context={}): Parameters ---------- - validate : boolean + validate : boolean or string If True (default), then validate the output dictionary - against the schema. + against the schema. If "deep" then recursively validate + all objects in the spec. This takes much more time, but + it results in friendlier tracebacks for large objects. ignore : list A list of keys to ignore. This will *not* passed to child to_dict function calls. @@ -166,10 +199,11 @@ def to_dict(self, validate=True, ignore=[], context={}): jsonschema.ValidationError : if validate=True and the dict does not conform to the schema """ - # TODO: add validate='once' and validate='deep' + sub_validate = 'deep' if validate == 'deep' else False + def _todict(val): if isinstance(val, SchemaBase): - return val.to_dict(validate=False, context=context) + return val.to_dict(validate=sub_validate, context=context) elif isinstance(val, list): return [_todict(v) for v in val] elif isinstance(val, dict): @@ -187,7 +221,10 @@ def _todict(val): raise ValueError("{0} instance has both a value and properties : " "cannot serialize to dict".format(self.__class__)) if validate: - self.validate(result) + try: + self.validate(result) + except jsonschema.ValidationError as err: + raise SchemaValidationError(self, err) return result @classmethod diff --git a/altair/vegalite/api.py b/altair/vegalite/api.py index 0ccb70ad5..18ffacec4 100644 --- a/altair/vegalite/api.py +++ b/altair/vegalite/api.py @@ -1 +1 @@ -from .v1.api import * +from .v2.api import * diff --git a/altair/vegalite/schema.py b/altair/vegalite/schema.py index b97ce5f65..147ab2aa2 100644 --- a/altair/vegalite/schema.py +++ b/altair/vegalite/schema.py @@ -1,3 +1,3 @@ """Altair schema wrappers""" -from .v1.schema import * +from .v2.schema import * diff --git a/altair/vegalite/v2/api.py b/altair/vegalite/v2/api.py index 2daeeefb1..a5b7fb55f 100644 --- a/altair/vegalite/v2/api.py +++ b/altair/vegalite/v2/api.py @@ -202,7 +202,16 @@ def to_dict(self, *args, **kwargs): context['data'] = original_data kwargs['context'] = context - dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs) + try: + dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs) + except jsonschema.ValidationError: + dct = None + + # If we hit an error, then re-convert with validate='deep' to get + # a more useful traceback + if dct is None: + kwargs['validate'] = 'deep' + dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs) if is_top_level: # since this is top-level we add $schema if it's missing @@ -412,7 +421,11 @@ def _wrap_in_channel_class(obj, prop): if 'value' in obj: clsname += 'Value' - cls = getattr(channels, clsname) + + try: + cls = getattr(channels, clsname) + except AttributeError: + raise ValueError("Unrecognized encoding channel '{0}'".format(prop)) try: # Don't force validation here; some objects won't be valid until diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py index b8950ede3..64b2f7f9f 100644 --- a/tools/schemapi/schemapi.py +++ b/tools/schemapi/schemapi.py @@ -1,8 +1,10 @@ import collections import contextlib import json +import textwrap import jsonschema +import six # If DEBUG_MODE is True, then schema objects are converted to dict and @@ -34,6 +36,35 @@ def debug_mode(arg): DEBUG_MODE = original +class SchemaValidationError(jsonschema.ValidationError): + """A wrapper for jsonschema.ValidationError with friendlier traceback""" + def __init__(self, obj, err): + super(SchemaValidationError, self).__init__(**err._contents()) + self.obj = obj + + def __unicode__(self): + cls = self.obj.__class__ + schema_path = ['{0}.{1}'.format(cls.__module__,cls.__name__)] + schema_path.extend(self.schema_path) + schema_path = ' ->'.join(val for val in schema_path[:-1] + if val not in ('properties', + 'additionalProperties', + 'patternProperties')) + return """Invalid specification + + validator {1!r} in {0} + + {2} + """.format(schema_path, self.validator, self.message) + + if six.PY3: + __str__ = __unicode__ + else: + def __str__(self): + return unicode(self).encode("utf-8") + + + class UndefinedType(object): """A singleton object for marking undefined attributes""" __instance = None @@ -144,9 +175,11 @@ def to_dict(self, validate=True, ignore=[], context={}): Parameters ---------- - validate : boolean + validate : boolean or string If True (default), then validate the output dictionary - against the schema. + against the schema. If "deep" then recursively validate + all objects in the spec. This takes much more time, but + it results in friendlier tracebacks for large objects. ignore : list A list of keys to ignore. This will *not* passed to child to_dict function calls. @@ -164,10 +197,11 @@ def to_dict(self, validate=True, ignore=[], context={}): jsonschema.ValidationError : if validate=True and the dict does not conform to the schema """ - # TODO: add validate='once' and validate='deep' + sub_validate = 'deep' if validate == 'deep' else False + def _todict(val): if isinstance(val, SchemaBase): - return val.to_dict(validate=False, context=context) + return val.to_dict(validate=sub_validate, context=context) elif isinstance(val, list): return [_todict(v) for v in val] elif isinstance(val, dict): @@ -185,7 +219,10 @@ def _todict(val): raise ValueError("{0} instance has both a value and properties : " "cannot serialize to dict".format(self.__class__)) if validate: - self.validate(result) + try: + self.validate(result) + except jsonschema.ValidationError as err: + raise SchemaValidationError(self, err) return result @classmethod From 3ca57eba6678ca214ae0096498b3f1606d4695f9 Mon Sep 17 00:00:00 2001 From: Jake VanderPlas Date: Mon, 5 Mar 2018 13:05:12 -0800 Subject: [PATCH 2/2] Reword schema error message --- altair/utils/schemapi.py | 2 +- tools/schemapi/schemapi.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py index 5a0696997..32bac9664 100644 --- a/altair/utils/schemapi.py +++ b/altair/utils/schemapi.py @@ -54,7 +54,7 @@ def __unicode__(self): 'patternProperties')) return """Invalid specification - validator {1!r} in {0} + {0}, validating {1!r} {2} """.format(schema_path, self.validator, self.message) diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py index 64b2f7f9f..c6143bb12 100644 --- a/tools/schemapi/schemapi.py +++ b/tools/schemapi/schemapi.py @@ -52,7 +52,7 @@ def __unicode__(self): 'patternProperties')) return """Invalid specification - validator {1!r} in {0} + {0}, validating {1!r} {2} """.format(schema_path, self.validator, self.message)