From b85767069055eecf6f9b6a90bd717b72d170dbf9 Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Mon, 12 Nov 2018 10:14:48 -0600 Subject: [PATCH 01/25] Prototype of new and improved options mechanism --- holoviews/core/dimension.py | 3 +++ holoviews/core/options.py | 28 +++++++++++++++++--- holoviews/util/__init__.py | 53 +++++++++++++++++++++++++++++++++++-- holoviews/util/settings.py | 2 +- 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index f6d22712c8..edcadba309 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -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 " diff --git a/holoviews/core/options.py b/holoviews/core/options.py index dbbfdae690..a3e92d5480 100644 --- a/holoviews/core/options.py +++ b/holoviews/core/options.py @@ -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 = [] @@ -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) @@ -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) @@ -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): @@ -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): diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index f6b7649bd4..2003ae176b 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -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 @@ -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 @@ -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.') @@ -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: @@ -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): """ @@ -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): diff --git a/holoviews/util/settings.py b/holoviews/util/settings.py index eea8ab64b7..6699481403 100644 --- a/holoviews/util/settings.py +++ b/holoviews/util/settings.py @@ -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 From ba166355d747d37d134522527471b847ae154ded Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 12 Nov 2018 14:17:15 -0600 Subject: [PATCH 02/25] Removed validation from opts completers --- holoviews/util/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index 2003ae176b..59d278dc33 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -207,10 +207,6 @@ def _options_error(cls, opt, objtype, backend, valid_options): @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) From f11faeb96d79cdf339fada6b4319a2d75908810f Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 12 Nov 2018 14:17:58 -0600 Subject: [PATCH 03/25] Skipping processing of dotted OptionTree entries --- holoviews/util/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index 59d278dc33..f72a40046a 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -218,7 +218,8 @@ def fn(spec=None, **kws): def _update_backend(cls, backend): backend_options = Store.options(backend) all_keywords = set() - for element in backend_options.keys(): # What if dotted? + for element in backend_options.keys(): + if '.' in element: continue element_keywords = [] options = backend_options['.'.join(element)] for group in Options._expected_groups: @@ -233,8 +234,8 @@ def _update_backend(cls, backend): 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) From 35d943b83a23eb6902c07a56f3d825bdb0d79af6 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 12 Nov 2018 14:59:27 -0600 Subject: [PATCH 04/25] Updated .options to process *args --- holoviews/core/dimension.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index edcadba309..64d5a4bfa9 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -1222,7 +1222,7 @@ def opts(self, options=None, backend=None, clone=True, **kwargs): return obj - def options(self, options=None, backend=None, clone=True, **kwargs): + def options(self, *args, backend=None, clone=True, **kwargs): """ Applies options on an object or nested group of objects in a flat format returning a new object with the options @@ -1243,12 +1243,16 @@ def options(self, options=None, backend=None, clone=True, **kwargs): If no options are supplied all options on the object will be reset. Disabling clone will modify the object inplace. """ - if isinstance(options, basestring): + + if len(args) == 0: + options = None + elif isinstance(args[0], basestring): options = {options: kwargs} - elif isinstance(options, list): + elif isinstance(args[0], list): if kwargs: raise ValueError('Please specify a list of option objects, or kwargs, but not both') - elif options and kwargs: + options = list(args) + elif args and kwargs: raise ValueError("Options must be defined in one of two formats." "Either supply keywords defining the options for " "the current object, e.g. obj.options(cmap='viridis'), " From 5a26dec3e1f9053a2b186b0ea1a432ca8c3e21b4 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 12 Nov 2018 15:00:35 -0600 Subject: [PATCH 05/25] Now supporting a mixture of Option objects and dictionaries --- holoviews/util/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index f72a40046a..6c315da969 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -131,10 +131,14 @@ def expand_options(cls, options, backend=None): backend_options = Store.options(backend=backend or current_backend) expanded = {} if isinstance(options, list): - options = {} + merged_options = {} for obj in options: - options[obj.key] = obj.kwargs - + if isinstance(obj,dict): + merged_options =dict(merged_options, **obj) + else: + merged_options[obj.key] = obj.kwargs + options = merged_options + for objspec, options in options.items(): objtype = objspec.split('.')[0] if objtype not in backend_options: From ab170aaad9b61e0fb983ed6cd127be4a9e518f5e Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 12 Nov 2018 15:15:55 -0600 Subject: [PATCH 06/25] Fixed hole in Options repr --- holoviews/core/options.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/core/options.py b/holoviews/core/options.py index a3e92d5480..7cbdecfc47 100644 --- a/holoviews/core/options.py +++ b/holoviews/core/options.py @@ -503,8 +503,10 @@ def options(self): def __repr__(self): kws = ', '.join("%s=%r" % (k,self.kwargs[k]) for k in sorted(self.kwargs.keys())) - if self.key and self.key[0].isupper(): + if self.key and self.key[0].isupper() and kws: return "%s(%s, %s)" % (self.__class__.__name__, repr(self.key), kws) + elif self.key and self.key[0].isupper(): + return "%s(%s)" % (self.__class__.__name__, repr(self.key)) else: return "%s(%s)" % (self.__class__.__name__, kws) From 441108b2dbe37a2f17e74a92a9f39e4579f7baa3 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 12 Nov 2018 15:18:44 -0600 Subject: [PATCH 07/25] Fixed bug in options *args processing --- holoviews/core/dimension.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 64d5a4bfa9..62330e4241 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -1251,7 +1251,7 @@ def options(self, *args, backend=None, clone=True, **kwargs): elif isinstance(args[0], list): if kwargs: raise ValueError('Please specify a list of option objects, or kwargs, but not both') - options = list(args) + options = args[0] elif args and kwargs: raise ValueError("Options must be defined in one of two formats." "Either supply keywords defining the options for " @@ -1259,6 +1259,8 @@ def options(self, *args, backend=None, clone=True, **kwargs): "or explicitly define the type, e.g." "obj.options({'Image': {'cmap': 'viridis'}})." "Supplying both formats is not supported.") + elif args: + options = list(args) elif kwargs: options = {type(self).__name__: kwargs} From e11a964a5188541176d6b4a885bc01041e87a962 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 12 Nov 2018 15:31:14 -0600 Subject: [PATCH 08/25] Now correctly merging options --- holoviews/util/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index 6c315da969..b7a0096249 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -127,6 +127,7 @@ def expand_options(cls, options, backend=None): {'Image': {'plot': dict(show_title=False), 'style': dict(cmap='viridis')}} """ + from .parser import OptsSpec # Move this utility! current_backend = Store.current_backend backend_options = Store.options(backend=backend or current_backend) expanded = {} @@ -134,9 +135,11 @@ def expand_options(cls, options, backend=None): merged_options = {} for obj in options: if isinstance(obj,dict): - merged_options =dict(merged_options, **obj) + new_opts = obj else: - merged_options[obj.key] = obj.kwargs + new_opts = {obj.key: obj.kwargs} + + merged_options = OptsSpec._merge_options(merged_options, new_opts) options = merged_options for objspec, options in options.items(): From c842376675cc71571d3a776dc7ffc9e815c24a89 Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Mon, 12 Nov 2018 22:28:05 -0600 Subject: [PATCH 09/25] Fixed .options signature for Python 2 --- holoviews/core/dimension.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 62330e4241..413d61d3c2 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -1222,7 +1222,7 @@ def opts(self, options=None, backend=None, clone=True, **kwargs): return obj - def options(self, *args, backend=None, clone=True, **kwargs): + def options(self, *args, **kwargs): """ Applies options on an object or nested group of objects in a flat format returning a new object with the options @@ -1243,7 +1243,9 @@ def options(self, *args, backend=None, clone=True, **kwargs): If no options are supplied all options on the object will be reset. Disabling clone will modify the object inplace. """ - + backend = kwargs.pop('backend', None) + clone = kwargs.pop('clone', True) + if len(args) == 0: options = None elif isinstance(args[0], basestring): From 721ae2206a461e483ad4d162c4b49ba9bf9475be Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Mon, 12 Nov 2018 23:08:15 -0600 Subject: [PATCH 10/25] Fixed processing of *args in Dimensioned .options method --- holoviews/core/dimension.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 413d61d3c2..b6d8ff5f5b 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -1246,11 +1246,11 @@ def options(self, *args, **kwargs): backend = kwargs.pop('backend', None) clone = kwargs.pop('clone', True) - if len(args) == 0: + if len(args) == 0 and len(kwargs)==0: options = None - elif isinstance(args[0], basestring): + elif args and isinstance(args[0], basestring): options = {options: kwargs} - elif isinstance(args[0], list): + elif args and isinstance(args[0], list): if kwargs: raise ValueError('Please specify a list of option objects, or kwargs, but not both') options = args[0] From f354876b470b5ddbaf17631c339e71a10e1eb4cb Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Mon, 12 Nov 2018 23:08:48 -0600 Subject: [PATCH 11/25] Updated .options signature on HoloMap and DynamicMap --- holoviews/core/spaces.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index de9be055c9..444cac99e1 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -97,7 +97,7 @@ def opts(self, options=None, backend=None, clone=True, **kwargs): return self.clone(data) - def options(self, options=None, backend=None, clone=True, **kwargs): + def options(self, *args, **kwargs): """ Applies options on an object or nested group of objects in a flat format returning a new object with the options @@ -118,7 +118,7 @@ def options(self, options=None, backend=None, clone=True, **kwargs): If no options are supplied all options on the object will be reset. Disabling clone will modify the object inplace. """ - data = OrderedDict([(k, v.options(options, backend, clone, **kwargs)) + data = OrderedDict([(k, v.options(*args, **kwargs)) for k, v in self.data.items()]) return self.clone(data) @@ -946,7 +946,7 @@ def opts(self, options=None, backend=None, clone=True, **kwargs): return dmap - def options(self, options=None, backend=None, clone=True, **kwargs): + def options(self, *args, **kwargs): """ Applies options on an object or nested group of objects in a flat format returning a new object with the options @@ -968,9 +968,10 @@ def options(self, options=None, backend=None, clone=True, **kwargs): Disabling clone will modify the object inplace. """ from ..util import Dynamic + clone = kwargs.get('clone', True) + obj = self if clone else self.clone() - dmap = Dynamic(obj, operation=lambda obj, **dynkwargs: obj.options(options, backend, - clone, **kwargs), + dmap = Dynamic(obj, operation=lambda obj, **dynkwargs: obj.options(*args, **kwargs), streams=self.streams, link_inputs=True) if not clone: with util.disable_constant(self): @@ -978,7 +979,7 @@ def options(self, options=None, backend=None, clone=True, **kwargs): self.callback.inputs[:] = [obj] obj.callback.inputs[:] = [] dmap = self - dmap.data = OrderedDict([(k, v.options(options, backend, **kwargs)) + dmap.data = OrderedDict([(k, v.options(*args, **kwargs)) for k, v in self.data.items()]) return dmap From 22e4067d87ac9491c01001580407ef9a27fd14e8 Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Mon, 12 Nov 2018 23:25:59 -0600 Subject: [PATCH 12/25] Updated existing options unit tests --- holoviews/tests/core/testoptions.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/holoviews/tests/core/testoptions.py b/holoviews/tests/core/testoptions.py index 90f1bf5231..90b575c408 100644 --- a/holoviews/tests/core/testoptions.py +++ b/holoviews/tests/core/testoptions.py @@ -26,6 +26,14 @@ class TestOptions(ComparisonTestCase): + def setUp(self): + Options._expected_groups = ['test'] + super(TestOptions, self).setUp() + + def tearDown(self): + Options._expected_groups = ['style', 'plot', 'norm'] + super(TestOptions, self).tearDown() + def test_options_init(self): Options('test') @@ -114,6 +122,14 @@ def test_options_inherit_invalid_keywords(self): class TestCycle(ComparisonTestCase): + def setUp(self): + Options._expected_groups = ['test'] + super(TestCycle, self).setUp() + + def tearDown(self): + Options._expected_groups = ['style', 'plot', 'norm'] + super(TestCycle, self).tearDown() + def test_cycle_init(self): Cycle(values=['a', 'b', 'c']) Cycle(values=[1, 2, 3]) @@ -178,6 +194,14 @@ def test_options_property_disabled(self): class TestOptionTree(ComparisonTestCase): + def setUp(self): + Options._expected_groups = ['group1', 'group2'] + super(TestOptionTree, self).setUp() + + def tearDown(self): + Options._expected_groups = ['style', 'plot', 'norm'] + super(TestOptionTree, self).tearDown() + def test_optiontree_init_1(self): OptionTree(groups=['group1', 'group2']) @@ -532,6 +556,7 @@ def test_style_transfer(self): class TestOptionTreeFind(ComparisonTestCase): def setUp(self): + Options._expected_groups = ['group'] options = OptionTree(groups=['group']) self.opts1 = Options('group', kw1='value1') self.opts2 = Options('group', kw2='value2') @@ -553,6 +578,7 @@ def setUp(self): def tearDown(self): + Options._expected_groups = ['style', 'plot', 'norm'] Store.options(val=self.original_options) Store._custom_options = {k:{} for k in Store._custom_options.keys()} @@ -732,4 +758,3 @@ def test_pickle_mpl_bokeh(self): Store.current_backend = 'bokeh' bokeh_opts = Store.lookup_options('bokeh', img, 'style').options self.assertEqual(bokeh_opts, {'cmap':'Purple'}) - From 4a909ee3852f025a8193c18fa1ecab2ddd8f866c Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Mon, 12 Nov 2018 23:26:13 -0600 Subject: [PATCH 13/25] Improved message when using undefined lowercase key in Options --- holoviews/core/options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/core/options.py b/holoviews/core/options.py index 7cbdecfc47..3d47a0e051 100644 --- a/holoviews/core/options.py +++ b/holoviews/core/options.py @@ -405,8 +405,8 @@ def __init__(self, key=None, allowed_keywords=[], merge_keywords=True, max_cycle 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)) + raise Exception('Key %s does not start with a capitalized element class name and is not a group in %s' + % (repr(key), ', '.join(repr(el) for el in self._expected_groups))) for invalid_kw in invalid_kws: error = OptionError(invalid_kw, allowed_keywords, group_name=key) From ab2704148fd97b144cdf3ccea799661a6cdbe608 Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Mon, 12 Nov 2018 23:34:38 -0600 Subject: [PATCH 14/25] Renamed _expected_groups to _option_groups --- holoviews/core/options.py | 8 ++++---- holoviews/tests/core/testoptions.py | 16 ++++++++-------- holoviews/util/__init__.py | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/holoviews/core/options.py b/holoviews/core/options.py index 3d47a0e051..6be100f667 100644 --- a/holoviews/core/options.py +++ b/holoviews/core/options.py @@ -392,7 +392,7 @@ class Options(param.Parameterized): skipping over invalid keywords or not. May only be specified at the class level.""") - _expected_groups = ['style', 'plot', 'norm'] + _option_groups = ['style', 'plot', 'norm'] def __init__(self, key=None, allowed_keywords=[], merge_keywords=True, max_cycles=None, **kwargs): @@ -404,9 +404,9 @@ 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: + if key and key[0].islower() and key not in self._option_groups: raise Exception('Key %s does not start with a capitalized element class name and is not a group in %s' - % (repr(key), ', '.join(repr(el) for el in self._expected_groups))) + % (repr(key), ', '.join(repr(el) for el in self._option_groups))) for invalid_kw in invalid_kws: error = OptionError(invalid_kw, allowed_keywords, group_name=key) @@ -631,7 +631,7 @@ def __setattr__(self, identifier, val): 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)) + ', '.join(repr(el) for el in Options._option_groups)) elif isinstance(val, Options): group_items = {val.key: val} elif isinstance(val, OptionTree): diff --git a/holoviews/tests/core/testoptions.py b/holoviews/tests/core/testoptions.py index 90b575c408..5b10a48d66 100644 --- a/holoviews/tests/core/testoptions.py +++ b/holoviews/tests/core/testoptions.py @@ -27,11 +27,11 @@ class TestOptions(ComparisonTestCase): def setUp(self): - Options._expected_groups = ['test'] + Options.__option_groups = ['test'] super(TestOptions, self).setUp() def tearDown(self): - Options._expected_groups = ['style', 'plot', 'norm'] + Options.__option_groups = ['style', 'plot', 'norm'] super(TestOptions, self).tearDown() def test_options_init(self): @@ -123,11 +123,11 @@ def test_options_inherit_invalid_keywords(self): class TestCycle(ComparisonTestCase): def setUp(self): - Options._expected_groups = ['test'] + Options.__option_groups = ['test'] super(TestCycle, self).setUp() def tearDown(self): - Options._expected_groups = ['style', 'plot', 'norm'] + Options.__option_groups = ['style', 'plot', 'norm'] super(TestCycle, self).tearDown() def test_cycle_init(self): @@ -195,11 +195,11 @@ def test_options_property_disabled(self): class TestOptionTree(ComparisonTestCase): def setUp(self): - Options._expected_groups = ['group1', 'group2'] + Options.__option_groups = ['group1', 'group2'] super(TestOptionTree, self).setUp() def tearDown(self): - Options._expected_groups = ['style', 'plot', 'norm'] + Options.__option_groups = ['style', 'plot', 'norm'] super(TestOptionTree, self).tearDown() def test_optiontree_init_1(self): @@ -556,7 +556,7 @@ def test_style_transfer(self): class TestOptionTreeFind(ComparisonTestCase): def setUp(self): - Options._expected_groups = ['group'] + Options.__option_groups = ['group'] options = OptionTree(groups=['group']) self.opts1 = Options('group', kw1='value1') self.opts2 = Options('group', kw2='value2') @@ -578,7 +578,7 @@ def setUp(self): def tearDown(self): - Options._expected_groups = ['style', 'plot', 'norm'] + Options.__option_groups = ['style', 'plot', 'norm'] Store.options(val=self.original_options) Store._custom_options = {k:{} for k in Store._custom_options.keys()} diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index b7a0096249..e74844a6fc 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -229,7 +229,7 @@ def _update_backend(cls, backend): if '.' in element: continue element_keywords = [] options = backend_options['.'.join(element)] - for group in Options._expected_groups: + for group in Options._option_groups: element_keywords.extend(options[group].allowed_keywords) all_keywords |= set(element_keywords) From a828fb41d24e939cc846486a43a6aed759fac958 Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Mon, 12 Nov 2018 23:41:35 -0600 Subject: [PATCH 15/25] Fixed bug in .options and deleted trailing whitespace --- holoviews/core/dimension.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index b6d8ff5f5b..21f104792e 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -1245,11 +1245,11 @@ def options(self, *args, **kwargs): """ backend = kwargs.pop('backend', None) clone = kwargs.pop('clone', True) - + if len(args) == 0 and len(kwargs)==0: options = None elif args and isinstance(args[0], basestring): - options = {options: kwargs} + options = {args[0]: kwargs} elif args and isinstance(args[0], list): if kwargs: raise ValueError('Please specify a list of option objects, or kwargs, but not both') From 95c5a51ca22ae82f6b61c70ead6833a6f3f300aa Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Mon, 12 Nov 2018 23:44:55 -0600 Subject: [PATCH 16/25] Fixed bug in hv.opts validation --- holoviews/util/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index e74844a6fc..2087dcfb5f 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -77,7 +77,7 @@ class opts(param.ParameterizedFunction): def __call__(self, *args, **params): - if args and set(params.keys()) != set(['strict']): + 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) From d5620315032cd518acd07d30e69084bd301fdcac Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Tue, 13 Nov 2018 00:02:08 -0600 Subject: [PATCH 17/25] Fixed incorrect search and replace --- holoviews/tests/core/testoptions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/holoviews/tests/core/testoptions.py b/holoviews/tests/core/testoptions.py index 5b10a48d66..dbd638c68d 100644 --- a/holoviews/tests/core/testoptions.py +++ b/holoviews/tests/core/testoptions.py @@ -27,11 +27,11 @@ class TestOptions(ComparisonTestCase): def setUp(self): - Options.__option_groups = ['test'] + Options._option_groups = ['test'] super(TestOptions, self).setUp() def tearDown(self): - Options.__option_groups = ['style', 'plot', 'norm'] + Options._option_groups = ['style', 'plot', 'norm'] super(TestOptions, self).tearDown() def test_options_init(self): @@ -123,11 +123,11 @@ def test_options_inherit_invalid_keywords(self): class TestCycle(ComparisonTestCase): def setUp(self): - Options.__option_groups = ['test'] + Options._option_groups = ['test'] super(TestCycle, self).setUp() def tearDown(self): - Options.__option_groups = ['style', 'plot', 'norm'] + Options._option_groups = ['style', 'plot', 'norm'] super(TestCycle, self).tearDown() def test_cycle_init(self): @@ -195,11 +195,11 @@ def test_options_property_disabled(self): class TestOptionTree(ComparisonTestCase): def setUp(self): - Options.__option_groups = ['group1', 'group2'] + Options._option_groups = ['group1', 'group2'] super(TestOptionTree, self).setUp() def tearDown(self): - Options.__option_groups = ['style', 'plot', 'norm'] + Options._option_groups = ['style', 'plot', 'norm'] super(TestOptionTree, self).tearDown() def test_optiontree_init_1(self): @@ -556,7 +556,7 @@ def test_style_transfer(self): class TestOptionTreeFind(ComparisonTestCase): def setUp(self): - Options.__option_groups = ['group'] + Options._option_groups = ['group'] options = OptionTree(groups=['group']) self.opts1 = Options('group', kw1='value1') self.opts2 = Options('group', kw2='value2') @@ -578,7 +578,7 @@ def setUp(self): def tearDown(self): - Options.__option_groups = ['style', 'plot', 'norm'] + Options._option_groups = ['style', 'plot', 'norm'] Store.options(val=self.original_options) Store._custom_options = {k:{} for k in Store._custom_options.keys()} From 99663c2407c0b9bac609dd3ce8da50101c902a28 Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Tue, 13 Nov 2018 09:14:50 -0600 Subject: [PATCH 18/25] Moved merge_option_dicts utility out of util.parser --- holoviews/core/util.py | 25 +++++++++++++++++++++++++ holoviews/util/__init__.py | 5 ++--- holoviews/util/parser.py | 26 ++------------------------ 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 88c3d7e32d..46f49c137b 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -144,6 +144,31 @@ def default(self, obj): return id(obj) +def merge_option_dicts(old_opts, new_opts): + """ + Update the old_opts option dictionary with the options defined in + new_opts. Instead of a shallow update as would be performed by calling + old_opts.update(new_opts), this updates the dictionaries of all option + types separately. + + Given two dictionaries + old_opts = {'a': {'x': 'old', 'y': 'old'}} + and + new_opts = {'a': {'y': 'new', 'z': 'new'}, 'b': {'k': 'new'}} + this returns a dictionary + {'a': {'x': 'old', 'y': 'new', 'z': 'new'}, 'b': {'k': 'new'}} + """ + merged = dict(old_opts) + + for option_type, options in new_opts.items(): + if option_type not in merged: + merged[option_type] = {} + + merged[option_type].update(options) + + return merged + + class periodic(Thread): """ Run a callback count times with a given period without blocking. diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index 2087dcfb5f..0096cede20 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -5,7 +5,7 @@ from ..core import DynamicMap, HoloMap, Dimensioned, ViewableElement, StoreOptions, Store from ..core.options import options_policy, Keywords, Options from ..core.operation import Operation -from ..core.util import Aliases, basestring # noqa (API import) +from ..core.util import Aliases, basestring, merge_option_dicts # noqa (API import) from ..core.operation import OperationCallable from ..core.spaces import Callable from ..core import util @@ -127,7 +127,6 @@ def expand_options(cls, options, backend=None): {'Image': {'plot': dict(show_title=False), 'style': dict(cmap='viridis')}} """ - from .parser import OptsSpec # Move this utility! current_backend = Store.current_backend backend_options = Store.options(backend=backend or current_backend) expanded = {} @@ -139,7 +138,7 @@ def expand_options(cls, options, backend=None): else: new_opts = {obj.key: obj.kwargs} - merged_options = OptsSpec._merge_options(merged_options, new_opts) + merged_options = merge_option_dicts(merged_options, new_opts) options = merged_options for objspec, options in options.items(): diff --git a/holoviews/util/parser.py b/holoviews/util/parser.py index bd2a864643..591f2f96fd 100644 --- a/holoviews/util/parser.py +++ b/holoviews/util/parser.py @@ -15,6 +15,7 @@ import pyparsing as pp from ..core.options import Options, Cycle, Palette +from ..core.util import merge_option_dicts from ..operation import Compositor ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' @@ -284,30 +285,7 @@ def _group_paths_without_options(cls, line_parse_result): yield active_pathspecs, {} - @classmethod - def _merge_options(cls, old_opts, new_opts): - """ - Update the old_opts option dictionary with the options defined in - new_opts. Instead of a shallow update as would be performed by calling - old_opts.update(new_opts), this updates the dictionaries of all option - types separately. - - Given two dictionaries - old_opts = {'a': {'x': 'old', 'y': 'old'}} - and - new_opts = {'a': {'y': 'new', 'z': 'new'}, 'b': {'k': 'new'}} - this returns a dictionary - {'a': {'x': 'old', 'y': 'new', 'z': 'new'}, 'b': {'k': 'new'}} - """ - merged = dict(old_opts) - - for option_type, options in new_opts.items(): - if option_type not in merged: - merged[option_type] = {} - - merged[option_type].update(options) - return merged @classmethod def apply_deprecations(cls, path): @@ -356,7 +334,7 @@ def parse(cls, line, ns={}): options['style'] = {cls.aliases.get(k,k):v for k,v in opts.items()} for pathspec in pathspecs: - parse[pathspec] = cls._merge_options(parse.get(pathspec, {}), options) + parse[pathspec] = merge_option_dicts(parse.get(pathspec, {}), options) return { cls.apply_deprecations(path): { From 9d1b4b39ccf79260d9425ac842bc354d9fb6601d Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Tue, 13 Nov 2018 09:29:14 -0600 Subject: [PATCH 19/25] Set config.warn_options_call to True --- holoviews/core/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 46f49c137b..a9491fec7a 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -84,7 +84,7 @@ class Config(param.ParameterizedFunction): Switch to the default style options used up to (and including) the HoloViews 1.7 release.""") - warn_options_call = param.Boolean(default=False, doc=""" + warn_options_call = param.Boolean(default=True, doc=""" Whether to warn when the deprecated __call__ options syntax is used (the opts method should now be used instead). It is recommended that users switch this on to update any uses of From 347ee9a755f3d36fa155eaf67e84180d76624e68 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 13 Nov 2018 13:40:21 -0600 Subject: [PATCH 20/25] Updated warning issued when deprecated __call__ is used --- holoviews/core/dimension.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 21f104792e..7f112a1b36 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -1153,7 +1153,9 @@ def __unicode__(self): def __call__(self, options=None, **kwargs): if config.warn_options_call: self.warning('Use of __call__ to set options will be deprecated ' - 'in future. Use the equivalent opts method instead.') + 'in future. Use the equivalent opts method or use " + "the recommended .options method instead.') + return self.opts(options, **kwargs) def opts(self, options=None, backend=None, clone=True, **kwargs): From 34f2318c575ca64352a2661a9d449c4ba37ad690 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 13 Nov 2018 13:48:52 -0600 Subject: [PATCH 21/25] Using set_current_backend in plotting extensions --- holoviews/plotting/bokeh/__init__.py | 2 +- holoviews/plotting/mpl/__init__.py | 2 +- holoviews/plotting/plotly/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 861b30082b..2c11930a6b 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -52,7 +52,7 @@ Store.renderers['bokeh'] = BokehRenderer.instance() if len(Store.renderers) == 1: - Store.current_backend = 'bokeh' + Store.set_current_backend('bokeh') associations = {Overlay: OverlayPlot, NdOverlay: OverlayPlot, diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index df29962969..ebde710c2e 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -97,7 +97,7 @@ def get_color_cycle(): Store.renderers['matplotlib'] = MPLRenderer.instance() if len(Store.renderers) == 1: - Store.current_backend = 'matplotlib' + Store.set_current_backend('matplotlib') # Defines a wrapper around GridPlot and RasterGridPlot # switching to RasterGridPlot if the plot only contains diff --git a/holoviews/plotting/plotly/__init__.py b/holoviews/plotting/plotly/__init__.py index 35ba93d62f..0d20a9d7c7 100644 --- a/holoviews/plotting/plotly/__init__.py +++ b/holoviews/plotting/plotly/__init__.py @@ -14,7 +14,7 @@ Store.renderers['plotly'] = PlotlyRenderer.instance() if len(Store.renderers) == 1: - Store.current_backend = 'plotly' + Store.set_current_backend('plotly') Store.register({Points: PointPlot, Scatter: PointPlot, From f328defcb875115d23c046979780deffcdc541d8 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 13 Nov 2018 13:54:37 -0600 Subject: [PATCH 22/25] Fixed warning string --- holoviews/core/dimension.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 7f112a1b36..e4bba484d1 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -1153,8 +1153,8 @@ def __unicode__(self): def __call__(self, options=None, **kwargs): if config.warn_options_call: self.warning('Use of __call__ to set options will be deprecated ' - 'in future. Use the equivalent opts method or use " - "the recommended .options method instead.') + 'in future. Use the equivalent opts method or use ' + 'the recommended .options method instead.') return self.opts(options, **kwargs) From 6a11776a856a823d4dce9fc3195db65b09d1a953 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 13 Nov 2018 14:01:05 -0600 Subject: [PATCH 23/25] Made the _update_backends hook robust to unavailable backends --- holoviews/core/options.py | 2 +- holoviews/util/__init__.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/core/options.py b/holoviews/core/options.py index 6be100f667..896d9950a1 100644 --- a/holoviews/core/options.py +++ b/holoviews/core/options.py @@ -1065,7 +1065,7 @@ class Store(object): load_counter_offset = None save_option_state = False - current_backend = 'matplotlib' # Would be nice to have a class property + current_backend = 'matplotlib' _backend_switch_hooks = [] diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index 0096cede20..d0bafe4093 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -222,6 +222,8 @@ def fn(spec=None, **kws): @classmethod def _update_backend(cls, backend): + if backend not in Store.loaded_backends(): + return backend_options = Store.options(backend) all_keywords = set() for element in backend_options.keys(): From 5900103dc3673b4e07272de83790f19c92467ccd Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Tue, 13 Nov 2018 23:15:23 -0600 Subject: [PATCH 24/25] Added tab completion to hv.output --- holoviews/util/settings.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/holoviews/util/settings.py b/holoviews/util/settings.py index 6699481403..db448f0861 100644 --- a/holoviews/util/settings.py +++ b/holoviews/util/settings.py @@ -214,20 +214,24 @@ def _generate_docstring(cls): holomap = "holomap : The display type for holomaps" widgets = "widgets : The widget mode for widgets" fps = "fps : The frames per second used for animations" - frames= ("max_frames : The max number of frames rendered (default %r)" - % cls.defaults['max_frames']) + max_frames= ("max_frames : The max number of frames rendered (default %r)" + % cls.defaults['max_frames']) size = "size : The percentage size of displayed output" dpi = "dpi : The rendered dpi of the figure" - chars = ("charwidth : The max character width for displaying helper (default %r)" + charwidth = ("charwidth : The max character width for displaying helper (default %r)" % cls.defaults['charwidth']) - fname = ("filename : The filename of the saved output, if any (default %r)" - % cls.defaults['filename']) - page = ("info : The information to page about the displayed objects (default %r)" - % cls.defaults['info']) + filename = ("filename : The filename of the saved output, if any (default %r)" + % cls.defaults['filename']) + info = ("info : The information to page about the displayed objects (default %r)" + % cls.defaults['info']) css = ("css : Optional css style attributes to apply to the figure image tag") - descriptions = [backend, fig, holomap, widgets, fps, frames, size, dpi, chars, fname, page, css] - return '\n'.join(intro + descriptions) + descriptions = [backend, fig, holomap, widgets, fps, max_frames, size, + dpi, charwidth, filename, info, css] + keywords = ['backend', 'fig', 'holomap', 'widgets', 'fps', 'max_frames', + 'size', 'dpi', 'charwidth', 'filename', 'info', 'css'] + signature = '\noutput(%s)\n' % ', '.join('%s=None' % kw for kw in keywords) + return '\n'.join([signature] + intro + descriptions) @classmethod From 5b3503c05f0d5bf4579facac6a9571b843953258 Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Tue, 13 Nov 2018 23:19:07 -0600 Subject: [PATCH 25/25] Preserving original docstring for hv.opts --- holoviews/util/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index d0bafe4093..3dbe99c95a 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -70,6 +70,8 @@ class opts(param.ParameterizedFunction): %%opts cell magic respectively. """ + __original_docstring__ = None + strict = param.Boolean(default=False, doc=""" Whether to be strict about the options specification. If not set to strict (default), any invalid keywords are simply skipped. If @@ -222,8 +224,13 @@ def fn(spec=None, **kws): @classmethod def _update_backend(cls, backend): + + if cls.__original_docstring__ is None: + cls.__original_docstring__ = cls.__doc__ + if backend not in Store.loaded_backends(): return + backend_options = Store.options(backend) all_keywords = set() for element in backend_options.keys(): @@ -239,9 +246,9 @@ def _update_backend(cls, backend): 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 + old_doc = cls.__original_docstring__.replace('params(strict=Boolean, name=String)','') + cls.__doc__ = '\n opts({kws})'.format(kws=kws) + old_doc Store._backend_switch_hooks.append(opts._update_backend)