From 90609e16337cf51d356d4c460f4cf615a4608b12 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 20 Sep 2019 12:27:25 +0200 Subject: [PATCH 1/4] Ensure Dynamic utility subscribes to dependent function --- holoviews/streams.py | 13 ++++++++++--- holoviews/util/__init__.py | 6 ++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/holoviews/streams.py b/holoviews/streams.py index 24d8a64c14..6ee54fca95 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -635,6 +635,10 @@ class Params(Stream): parameters = param.List([], constant=True, doc=""" Parameters on the parameterized to watch.""") + watch_only = param.Boolean(default=False, doc=""" + Whether the stream should only watch and not return the parameter + values in the contents method.""") + def __init__(self, parameterized=None, parameters=None, watch=True, **params): if util.param_version < '1.8.0' and watch: raise RuntimeError('Params stream requires param version >= 1.8.0, ' @@ -728,6 +732,8 @@ def update(self, **kwargs): @property def contents(self): + if self.watch_only: + return {} filtered = {(p.owner, p.name): getattr(p.owner, p.name) for p in self.parameters} return {self._rename.get((o, n), n): v for (o, n), v in filtered.items() if self._rename.get((o, n), True) is not None} @@ -741,6 +747,10 @@ class ParamMethod(Params): change. """ + watch_only = param.Boolean(default=True, readonly=True, doc=""" + Whether the stream should only watch and not return the parameter + values in the contents method.""") + def __init__(self, parameterized, parameters=None, watch=True, **params): if not util.is_param_method(parameterized): raise ValueError('ParamMethod stream expects a method on a ' @@ -752,9 +762,6 @@ def __init__(self, parameterized, parameters=None, watch=True, **params): parameters = [p.pobj for p in parameterized.param.params_depended_on(method.__name__)] super(ParamMethod, self).__init__(parameterized, parameters, watch, **params) - @property - def contents(self): - return {} diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index d4f5eac895..fbf11bc214 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -898,6 +898,12 @@ def _get_streams(self, map_obj, watch=True): for value in self.p.kwargs.values(): if util.is_param_method(value, has_deps=True): streams.append(value) + elif isinstance(value, FunctionType) and hasattr(value, '_dinfo'): + dependencies = list(value._dinfo.get('dependencies', [])) + dependencies += list(value._dinfo.get('kwargs', {}).values()) + params = [d for d in dependencies if isinstance(d, param.Parameter) + and isinstance(d.owner, param.Parameterized)] + streams.append(Params(parameters=params, watch_only=True)) valid, invalid = Stream._process_streams(streams) if invalid: From 54b7c4c231b36f28338c021ee4a8fdd4db000280 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 20 Sep 2019 12:35:36 +0200 Subject: [PATCH 2/4] Add test --- holoviews/tests/core/testapply.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/holoviews/tests/core/testapply.py b/holoviews/tests/core/testapply.py index edfaaa3a17..6bba3b097c 100644 --- a/holoviews/tests/core/testapply.py +++ b/holoviews/tests/core/testapply.py @@ -109,6 +109,29 @@ def test_element_apply_param_method_with_dependencies(self): pinst.label = 'Another label' self.assertEqual(applied[()], self.element.relabel('Another label')) + def test_element_apply_function_with_dependencies(self): + pinst = ParamClass() + + @param.depends(pinst.param.label) + def get_label(label): + return label + '!' + + applied = self.element.apply('relabel', label=get_label) + + # Check stream + self.assertEqual(len(applied.streams), 1) + stream = applied.streams[0] + self.assertIsInstance(stream, Params) + self.assertEqual(stream.parameters, [pinst.param.label]) + + # Check results + self.assertEqual(applied[()], self.element.relabel('Test!')) + + # Ensure subscriber gets called + stream.add_subscriber(lambda **kwargs: applied[()]) + pinst.label = 'Another label' + self.assertEqual(applied.last, self.element.relabel('Another label!')) + def test_element_apply_dynamic_with_param_method(self): pinst = ParamClass() applied = self.element.apply(lambda x, label: x.relabel(label), label=pinst.dynamic_label) From 69932a2b5924be032cfc547bd47a41f973208b28 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 20 Sep 2019 12:47:47 +0200 Subject: [PATCH 3/4] Fixed watch_only Params stream option --- holoviews/streams.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/holoviews/streams.py b/holoviews/streams.py index 6ee54fca95..f5736e1079 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -635,11 +635,7 @@ class Params(Stream): parameters = param.List([], constant=True, doc=""" Parameters on the parameterized to watch.""") - watch_only = param.Boolean(default=False, doc=""" - Whether the stream should only watch and not return the parameter - values in the contents method.""") - - def __init__(self, parameterized=None, parameters=None, watch=True, **params): + def __init__(self, parameterized=None, parameters=None, watch=True, watch_only=False, **params): if util.param_version < '1.8.0' and watch: raise RuntimeError('Params stream requires param version >= 1.8.0, ' 'to support watching parameters.') @@ -659,6 +655,7 @@ def __init__(self, parameterized=None, parameters=None, watch=True, **params): rename.update({(o, k): v for o in owners}) params['rename'] = rename + self._watch_only = watch_only super(Params, self).__init__(parameterized=parameterized, parameters=parameters, **params) self._memoize_counter = 0 self._events = [] @@ -732,7 +729,7 @@ def update(self, **kwargs): @property def contents(self): - if self.watch_only: + if self._watch_only: return {} filtered = {(p.owner, p.name): getattr(p.owner, p.name) for p in self.parameters} return {self._rename.get((o, n), n): v for (o, n), v in filtered.items() @@ -747,10 +744,6 @@ class ParamMethod(Params): change. """ - watch_only = param.Boolean(default=True, readonly=True, doc=""" - Whether the stream should only watch and not return the parameter - values in the contents method.""") - def __init__(self, parameterized, parameters=None, watch=True, **params): if not util.is_param_method(parameterized): raise ValueError('ParamMethod stream expects a method on a ' @@ -760,6 +753,7 @@ def __init__(self, parameterized, parameters=None, watch=True, **params): parameterized = util.get_method_owner(parameterized) if not parameters: parameters = [p.pobj for p in parameterized.param.params_depended_on(method.__name__)] + params['watch_only'] = True super(ParamMethod, self).__init__(parameterized, parameters, watch, **params) From 7b421e34c214121fa947dc5a9564d1f06b6c6f4c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 20 Sep 2019 13:21:59 +0200 Subject: [PATCH 4/4] Consistently resolve dependencies --- holoviews/core/accessors.py | 17 +++++++------- holoviews/core/util.py | 38 +++++++++++++++++++++++++++---- holoviews/tests/core/testapply.py | 10 ++++++++ holoviews/util/__init__.py | 18 ++------------- 4 files changed, 54 insertions(+), 29 deletions(-) diff --git a/holoviews/core/accessors.py b/holoviews/core/accessors.py index 56d7bb8e7b..2e30020da3 100644 --- a/holoviews/core/accessors.py +++ b/holoviews/core/accessors.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, unicode_literals from collections import OrderedDict +from types import FunctionType import param @@ -84,24 +85,22 @@ def function(object, **kwargs): params = {p: val for p, val in kwargs.items() if isinstance(val, param.Parameter) and isinstance(val.owner, param.Parameterized)} - param_methods = {p: val for p, val in kwargs.items() - if util.is_param_method(val, has_deps=True)} + + dependent_kws = any( + (isinstance(val, FunctionType) and hasattr(val, '_dinfo')) or + util.is_param_method(val, has_deps=True) for val in kwargs.values() + ) if dynamic is None: dynamic = (bool(streams) or isinstance(self._obj, DynamicMap) or util.is_param_method(function, has_deps=True) or - params or param_methods) + params or dependent_kws) if applies and dynamic: return Dynamic(self._obj, operation=function, streams=streams, kwargs=kwargs, link_inputs=link_inputs) elif applies: - inner_kwargs = dict(kwargs) - for k, v in kwargs.items(): - if util.is_param_method(v, has_deps=True): - inner_kwargs[k] = v() - elif k in params: - inner_kwargs[k] = getattr(v.owner, v.name) + inner_kwargs = util.resolve_dependent_kwargs(kwargs) if hasattr(function, 'dynamic'): inner_kwargs['dynamic'] = False return function(self._obj, **inner_kwargs) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 23a9e379ed..2f3e1db100 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -9,11 +9,12 @@ import unicodedata import datetime as dt -from distutils.version import LooseVersion as _LooseVersion -from functools import partial from collections import defaultdict from contextlib import contextmanager +from distutils.version import LooseVersion as _LooseVersion +from functools import partial from threading import Thread, Event +from types import FunctionType import numpy as np import param @@ -26,7 +27,7 @@ # Python3 compatibility if sys.version_info.major >= 3: import builtins as builtins # noqa (compatibility) - + basestring = str unicode = str long = int @@ -38,7 +39,7 @@ LooseVersion = _LooseVersion else: import __builtin__ as builtins # noqa (compatibility) - + basestring = basestring unicode = unicode from itertools import izip @@ -1466,6 +1467,35 @@ def is_param_method(obj, has_deps=False): return parameterized +def resolve_dependent_kwargs(kwargs): + """Resolves parameter dependencies in the supplied dictionary + + Resolves parameter values, Parameterized instance methods and + parameterized functions with dependencies in the supplied + dictionary. + + Args: + kwargs (dict): A dictionary of keyword arguments + + Returns: + A new dictionary with where any parameter dependencies have been + resolved. + """ + resolved = {} + for k, v in kwargs.items(): + if is_param_method(v, has_deps=True): + v = v() + elif isinstance(v, param.Parameter) and isinstance(v.owner, param.Parameterized): + v = getattr(v.owner, v.name) + elif isinstance(v, FunctionType) and hasattr(v, '_dinfo'): + deps = v._dinfo + args = (getattr(p.owner, p.name) for p in deps.get('dependencies', [])) + kwargs = {k: getattr(p.owner, p.name) for k, p in deps.get('kw', {}).items()} + v = v(*args, **kwargs) + resolved[k] = v + return resolved + + @contextmanager def disable_constant(parameterized): """ diff --git a/holoviews/tests/core/testapply.py b/holoviews/tests/core/testapply.py index 6bba3b097c..0c83a633c1 100644 --- a/holoviews/tests/core/testapply.py +++ b/holoviews/tests/core/testapply.py @@ -132,6 +132,16 @@ def get_label(label): pinst.label = 'Another label' self.assertEqual(applied.last, self.element.relabel('Another label!')) + def test_element_apply_function_with_dependencies_non_dynamic(self): + pinst = ParamClass() + + @param.depends(pinst.param.label) + def get_label(label): + return label + '!' + + applied = self.element.apply('relabel', dynamic=False, label=get_label) + self.assertEqual(applied, self.element.relabel('Test!')) + def test_element_apply_dynamic_with_param_method(self): pinst = ParamClass() applied = self.element.apply(lambda x, label: x.relabel(label), label=pinst.dynamic_label) diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index fbf11bc214..0fae8e1766 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -921,20 +921,6 @@ def _process(self, element, key=None, kwargs={}): else: return self.p.operation(element, **kwargs) - def _eval_kwargs(self): - """Evaluates any parameterized methods in the kwargs""" - evaled_kwargs = {} - for k, v in self.p.kwargs.items(): - if util.is_param_method(v): - v = v() - elif isinstance(v, FunctionType) and hasattr(v, '_dinfo'): - deps = v._dinfo - args = (getattr(p.owner, p.name) for p in deps.get('dependencies', [])) - kwargs = {k: getattr(p.owner, p.name) for k, p in deps.get('kw', {}).items()} - v = v(*args, **kwargs) - evaled_kwargs[k] = v - return evaled_kwargs - def _dynamic_operation(self, map_obj): """ Generate function to dynamically apply the operation. @@ -942,12 +928,12 @@ def _dynamic_operation(self, map_obj): """ if not isinstance(map_obj, DynamicMap): def dynamic_operation(*key, **kwargs): - kwargs = dict(self._eval_kwargs(), **kwargs) + kwargs = dict(util.resolve_dependent_kwargs(self.p.kwargs), **kwargs) obj = map_obj[key] if isinstance(map_obj, HoloMap) else map_obj return self._process(obj, key, kwargs) else: def dynamic_operation(*key, **kwargs): - kwargs = dict(self._eval_kwargs(), **kwargs) + kwargs = dict(util.resolve_dependent_kwargs(self.p.kwargs), **kwargs) if map_obj._posarg_keys and not key: key = tuple(kwargs[k] for k in map_obj._posarg_keys) return self._process(map_obj[key], key, kwargs)