Skip to content

Commit

Permalink
Implemented cross-backend keyword validation
Browse files Browse the repository at this point in the history
  • Loading branch information
jlstevens committed Apr 13, 2017
1 parent da53d14 commit 84fb568
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 16 deletions.
78 changes: 64 additions & 14 deletions holoviews/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import pickle
import traceback
from contextlib import contextmanager
from collections import OrderedDict
from collections import OrderedDict, defaultdict

import numpy as np

Expand Down Expand Up @@ -288,6 +288,8 @@ def __init__(self, key=None, allowed_keywords=None, merge_keywords=True, **kwarg
else:
raise OptionError(kwarg, allowed_keywords)

for invalid_kw in invalid_kws:
StoreOptions.record_option_error(OptionError(invalid_kw, allowed_keywords))
if invalid_kws and self.warn_on_skip:
self.warning("Invalid options %s, valid options are: %s"
% (repr(invalid_kws), str(sorted(list(set(allowed_keywords))))))
Expand Down Expand Up @@ -1051,7 +1053,7 @@ def register(cls, associations, backend, style_aliases={}):
class StoreOptions(object):
"""
A collection of utilities for advanced users for creating and
setting customized option tress on the Store. Designed for use by
setting customized option trees on the Store. Designed for use by
either advanced users or the %opts line and cell magics which use
this machinery.
Expand All @@ -1060,8 +1062,42 @@ class StoreOptions(object):
access it is best to minimize the number of methods implemented on
that class and implement the necessary utilities on StoreOptions
instead.
Lastly this class offers a means to record all OptionErrors
generated by an option specification. This is used for validation
purposes.
"""

#=======================#
# OptionError recording #
#=======================#

_errors_recorded = None
_option_class_settings = None

@classmethod
def start_recording_errors(cls):
"Start collected errors supplied via record_option_error method"
cls._option_class_settings = (Options.skip_invalid, Options.warn_on_skip)
(Options.skip_invalid, Options.warn_on_skip) = (True, False)
cls._errors_recorded = []

@classmethod
def stop_recording_errors(cls):
"Stop recording errors and return recorded errors"
if cls._errors_recorded is None:
raise Exception('Cannot stop recording before it is started')
recorded = cls._errors_recorded[:]
(Options.skip_invalid ,Options.warn_on_skip) = cls._option_class_settings
cls._errors_recorded = None
return recorded

@classmethod
def record_option_error(cls, error):
"Record an option error if currently recording"
if cls._errors_recorded is not None:
cls._errors_recorded.append(error)

#===============#
# ID management #
#===============#
Expand Down Expand Up @@ -1136,19 +1172,33 @@ def apply_customizations(cls, spec, options):
options[str(key)] = customization
return options


@classmethod
def validate_spec(cls, spec, skip=Options.skip_invalid):
"""
Given a specification, validated it against the default
options tree (Store.options). Only tends to be useful when
invalid keywords generate exceptions instead of skipping.
"""
if skip: return
options = OptionTree(items=Store.options().data.items(),
groups=Store.options().groups)
return cls.apply_customizations(spec, options)

def validate_spec(cls, spec, backends=None):
"""
Given a specification, validated it against the options tree for
the specified backends by raising OptionError for invalid
options. If backends is None, validates against all the
currently loaded backend.
Only useful when invalid keywords generate exceptions instead of
skipping i.e Options.skip_invalid is False.
"""
backends = Store.loaded_backends()if backends is None else backends

error_info = defaultdict(set)
for backend in backends:
cls.start_recording_errors()
options = OptionTree(items=Store.options(backend).data.items(),
groups=Store.options(backend).groups)
cls.apply_customizations(spec, options)
for error in cls.stop_recording_errors():
error_info[(error.invalid_keyword, backend)] |= set(error.allowed_keywords)

for key in set(k for k,_ in error_info.keys()):
if set([(key, b) for b in backends]).issubset(set(error_info.keys())):
all_allowed = [ error_info[(key,b)] for b in backends ]
allowed_union = set().union(*all_allowed)
raise OptionError(key, allowed_keywords=sorted(allowed_union))

@classmethod
def expand_compositor_keys(cls, spec):
Expand Down
4 changes: 2 additions & 2 deletions holoviews/ipython/magics.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,8 +597,8 @@ def process_element(cls, obj):

@classmethod
def _format_options_error(cls, err):
info = (err.invalid_keyword, err.group_name, ', '.join(err.allowed_keywords))
return "Keyword <b>%r</b> not one of following %s options:<br><br><b>%s</b>" % info
info = (err.invalid_keyword, ', '.join(err.allowed_keywords))
return "Keyword <b>%r</b> not supported by any of the loaded backends. Valid options are :<br><br><b>%s</b>" % info

@classmethod
def register_custom_spec(cls, spec):
Expand Down

0 comments on commit 84fb568

Please sign in to comment.