From e7bfad92357fdde33c45faab755e0aaeb52b610a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 20 Sep 2019 13:37:48 +0200 Subject: [PATCH] Ensure Dynamic utility subscribes to dependent function (#3980) --- doc/nbpublisher | 2 +- holoviews/core/accessors.py | 17 +++++++------- holoviews/core/util.py | 39 +++++++++++++++++++++++++++---- holoviews/streams.py | 9 +++---- holoviews/tests/core/testapply.py | 33 ++++++++++++++++++++++++++ holoviews/util/__init__.py | 24 +++++++------------ 6 files changed, 90 insertions(+), 34 deletions(-) diff --git a/doc/nbpublisher b/doc/nbpublisher index 90ed382834..0ffe6a0fde 160000 --- a/doc/nbpublisher +++ b/doc/nbpublisher @@ -1 +1 @@ -Subproject commit 90ed3828347afd8bb93cd3183733fedb26a214a4 +Subproject commit 0ffe6a0fde289cffe51efa3776565bfd75b5633d 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 1418b7cdf1..f1ce35a38e 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -7,12 +7,14 @@ import string, fnmatch import unicodedata import datetime as dt + from collections import defaultdict -from functools import partial 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 +28,7 @@ # Python3 compatibility if sys.version_info.major >= 3: import builtins as builtins # noqa (compatibility) - + basestring = str unicode = str long = int @@ -38,7 +40,7 @@ LooseVersion = _LooseVersion else: import __builtin__ as builtins # noqa (compatibility) - + basestring = basestring unicode = unicode from itertools import izip @@ -1482,6 +1484,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/streams.py b/holoviews/streams.py index cf379e9462..916abbfbf8 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -637,7 +637,7 @@ class Params(Stream): parameters = param.List([], constant=True, doc=""" Parameters on the parameterized to watch.""") - 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.') @@ -657,6 +657,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 = [] @@ -730,6 +731,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} @@ -752,11 +755,9 @@ 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) - @property - def contents(self): - return {} diff --git a/holoviews/tests/core/testapply.py b/holoviews/tests/core/testapply.py index edfaaa3a17..0c83a633c1 100644 --- a/holoviews/tests/core/testapply.py +++ b/holoviews/tests/core/testapply.py @@ -109,6 +109,39 @@ 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_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 a87ad9a034..e927f7a223 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -894,6 +894,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: @@ -911,20 +917,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. @@ -932,12 +924,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)