Skip to content

Commit

Permalink
Prototype of new and improved options mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
jlstevens committed Nov 12, 2018
1 parent 517d925 commit b857670
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 6 deletions.
3 changes: 3 additions & 0 deletions holoviews/core/dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,9 @@ def options(self, options=None, backend=None, clone=True, **kwargs):
"""
if isinstance(options, basestring):
options = {options: kwargs}
elif isinstance(options, list):
if kwargs:
raise ValueError('Please specify a list of option objects, or kwargs, but not both')
elif options and kwargs:
raise ValueError("Options must be defined in one of two formats."
"Either supply keywords defining the options for "
Expand Down
28 changes: 25 additions & 3 deletions holoviews/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ class Options(param.Parameterized):
skipping over invalid keywords or not. May only be specified at
the class level.""")

_expected_groups = ['style', 'plot', 'norm']

def __init__(self, key=None, allowed_keywords=[], merge_keywords=True, max_cycles=None, **kwargs):

invalid_kws = []
Expand All @@ -402,6 +404,10 @@ def __init__(self, key=None, allowed_keywords=[], merge_keywords=True, max_cycle
else:
raise OptionError(kwarg, allowed_keywords)

if key and key[0].islower() and key not in self._expected_groups:
raise Exception('Key does not start with a capitalized element class name and is not a group in %s'
% ', '.join(repr(el) for el in self._expected_groups))

for invalid_kw in invalid_kws:
error = OptionError(invalid_kw, allowed_keywords, group_name=key)
StoreOptions.record_skipped_option(error)
Expand Down Expand Up @@ -495,8 +501,12 @@ def options(self):


def __repr__(self):
kws = ', '.join("%s=%r" % (k,v) for (k,v) in self.kwargs.items())
return "%s(%s)" % (self.__class__.__name__, kws)
kws = ', '.join("%s=%r" % (k,self.kwargs[k]) for k in sorted(self.kwargs.keys()))

if self.key and self.key[0].isupper():
return "%s(%s, %s)" % (self.__class__.__name__, repr(self.key), kws)
else:
return "%s(%s)" % (self.__class__.__name__, kws)

def __str__(self):
return repr(self)
Expand Down Expand Up @@ -617,6 +627,9 @@ def __setattr__(self, identifier, val):
group_items = val
elif isinstance(val, Options) and val.key is None:
raise AttributeError("Options object needs to have a group name specified.")
elif isinstance(val, Options) and val.key[0].isupper():
raise AttributeError("OptionTree only accepts Options using keys that are one of %s." %
', '.join(repr(el) for el in Options._expected_groups))
elif isinstance(val, Options):
group_items = {val.key: val}
elif isinstance(val, OptionTree):
Expand Down Expand Up @@ -1050,7 +1063,16 @@ class Store(object):
load_counter_offset = None
save_option_state = False

current_backend = 'matplotlib'
current_backend = 'matplotlib' # Would be nice to have a class property

_backend_switch_hooks = []

@classmethod
def set_current_backend(cls, backend):
"Use this method to set the backend to run the switch hooks"
for hook in cls._backend_switch_hooks:
hook(backend)
cls.current_backend = backend

@classmethod
def options(cls, backend=None, val=None):
Expand Down
53 changes: 51 additions & 2 deletions holoviews/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import param

from ..core import DynamicMap, HoloMap, Dimensioned, ViewableElement, StoreOptions, Store
from ..core.options import options_policy, Keywords
from ..core.options import options_policy, Keywords, Options
from ..core.operation import Operation
from ..core.util import Aliases, basestring # noqa (API import)
from ..core.operation import OperationCallable
Expand Down Expand Up @@ -37,6 +37,7 @@ def examples(path='holoviews-examples', verbose=False, force=False, root=__file_
print('Cannot find %s' % tree_root)



class opts(param.ParameterizedFunction):
"""
Utility function to set options either at the global level or on a
Expand Down Expand Up @@ -75,6 +76,12 @@ class opts(param.ParameterizedFunction):
strict, invalid keywords prevent the options being applied.""")

def __call__(self, *args, **params):

if args and set(params.keys()) != set(['strict']):
raise TypeError('When used with positional arguments, hv.opts accepts only strings and dictionaries, not keywords.')
if params and not args:
return Options(**params)

p = param.ParamOverrides(self, params)
if len(args) not in [1,2]:
raise TypeError('The opts utility accepts one or two positional arguments.')
Expand Down Expand Up @@ -123,6 +130,11 @@ def expand_options(cls, options, backend=None):
current_backend = Store.current_backend
backend_options = Store.options(backend=backend or current_backend)
expanded = {}
if isinstance(options, list):
options = {}
for obj in options:
options[obj.key] = obj.kwargs

for objspec, options in options.items():
objtype = objspec.split('.')[0]
if objtype not in backend_options:
Expand Down Expand Up @@ -192,6 +204,43 @@ def _options_error(cls, opt, objtype, backend, valid_options):
'across all extensions. No similar options '
'found.' % (opt, objtype))

@classmethod
def _build_completer(cls, element, allowed):
def fn(spec=None, **kws):
diff = set(kws.keys()) - set(allowed)
if diff:
raise Exception('Keywords %s not accepted by %r backend' % (', '.join(repr(el) for el in sorted(diff)),
Store.current_backend))
spec = element if spec is None else '%s.%s' % (element, spec)
return Options(spec, **kws)

kws = ', '.join('{opt}=None'.format(opt=opt) for opt in sorted(allowed))
fn.__doc__ = '{element}({kws})'.format(element=element, kws=kws)
return fn

@classmethod
def _update_backend(cls, backend):
backend_options = Store.options(backend)
all_keywords = set()
for element in backend_options.keys(): # What if dotted?
element_keywords = []
options = backend_options['.'.join(element)]
for group in Options._expected_groups:
element_keywords.extend(options[group].allowed_keywords)

all_keywords |= set(element_keywords)
with param.logging_level('CRITICAL'):
setattr(cls, element[0],
cls._build_completer(element[0],
element_keywords))


kws = ', '.join('{opt}=None'.format(opt=opt) for opt in sorted(all_keywords))
cls.__doc__ = 'opts({kws})'.format(kws=kws) # Keep original docstring


Store._backend_switch_hooks.append(opts._update_backend)


class output(param.ParameterizedFunction):
"""
Expand Down Expand Up @@ -336,7 +385,7 @@ def __call__(self, *args, **params):

if selected_backend is None:
raise ImportError('None of the backends could be imported')
Store.current_backend = selected_backend
Store.set_current_backend(selected_backend)


class Dynamic(param.ParameterizedFunction):
Expand Down
2 changes: 1 addition & 1 deletion holoviews/util/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ def initialize(cls, backend_list):
@classmethod
def set_backend(cls, backend):
cls.last_backend = Store.current_backend
Store.current_backend = backend
Store.set_current_backend(backend)


@classmethod
Expand Down

0 comments on commit b857670

Please sign in to comment.